From 8fc31b1e4e340bef600cdbde0fedac1702224d35 Mon Sep 17 00:00:00 2001
From: Mateo Wang <277851410+mateo-berri@users.noreply.github.com>
Date: Wed, 7 Oct 2026 21:41:03 -0700
Subject: [PATCH] feat(decisions): serve System One format at /v1/systemone and
OpenAI format at /v1/decisions (#45184)
* feat(decisions): serve System One format at /v1/systemone and OpenAI format at /v1/decisions
The System One request format moves to /v1/systemone and /systemone. /v1/decisions and
/decisions now accept the OpenAI Decisions API format, translate it into a System One
request, route it through the same pipeline, and translate the answers back.
* fix(decisions): import assert_never from typing_extensions for Python 3.10
* fix(decisions): cap OpenAI-format questions at the System One limit
/v1/decisions accepted up to 200 questions, but the System One request it translates to takes at most 128, so 129 to 200 questions failed with an internal validation error. Both now share MAX_DECISION_QUESTIONS.
* fix(decisions): return 400 for bodies that are not JSON
* test(decisions): send System One bodies to /v1/systemone in integration tests
* fix(decisions): return guardrail_information on OpenAI-format decisions when requested
* refactor(decisions): validate proxy request data before reading guardrail settings
---
gateway/routes/allowlist.py | 2 +
litellm/decisions/openai_transformation.py | 127 +++++++++++
litellm/proxy/_lazy_features.py | 2 +-
litellm/proxy/_lazy_openapi_snapshot.json | 48 ++++
litellm/proxy/_types.py | 2 +
.../auth/managed_authorization.py | 2 +
.../proxy/decisions_endpoints/endpoints.py | 145 ++++++++++---
litellm/types/decisions.py | 171 ++++++++++++++-
litellm/types/utils.py | 2 +
.../cost_calculation/cost_tracking_case.py | 2 +-
.../cost_calculation/cost_tracking_cases.json | 2 +-
.../cost_calculation/test_cost_tracking.py | 2 +-
.../providers/test_decisions_chaos.py | 4 +-
.../providers/test_decisions_wire.py | 14 +-
.../decisions/test_openai_transformation.py | 32 +++
.../decisions_endpoints/test_endpoints.py | 205 +++++++++++++++++-
.../SystemOneUI.integration.test.tsx | 16 +-
.../components/systemOneUI/SystemOneUI.tsx | 16 +-
.../systemOneUI/lib/decisions.test.ts | 2 +-
.../components/systemOneUI/lib/schemas.ts | 2 +-
.../systemOneUI/lib/validatePayload.ts | 2 +-
ui/litellm-dashboard/src/lib/http/schema.d.ts | 74 +++++++
22 files changed, 803 insertions(+), 71 deletions(-)
create mode 100644 litellm/decisions/openai_transformation.py
create mode 100644 tests/unit/decisions/test_openai_transformation.py
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;