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

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

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

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

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

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

---------

Co-authored-by: kerry <kerry@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
devin-ai-integration[bot] 2026-10-10 21:04:59 +00:00 • committed by GitHub
parent 5b6ecab52d
commit bc2cc42340
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 2961 additions and 80 deletions

File diff suppressed because it is too large Load diff

View file

@ -437,3 +437,41 @@ class CustomOpenAPISpec:
# Add responses API request schema
return CustomOpenAPISpec.add_responses_api_request_schema(with_embeddings)
_MAX_INLINE_DEPTH: Final = 32
def _inlined(node: JsonValue, defs: Mapping[str, JsonValue], depth: int = 0) -> JsonValue:
if depth >= _MAX_INLINE_DEPTH or not isinstance(node, (list, dict)):
return node
if isinstance(node, list):
return [_inlined(item, defs, depth + 1) for item in node]
ref: Final = node.get("$ref")
if isinstance(ref, str) and ref.startswith("#/$defs/"):
target: Final = _inlined(defs[ref.removeprefix("#/$defs/")], defs, depth + 1)
siblings: Final = {key: _inlined(value, defs, depth + 1) for key, value in node.items() if key != "$ref"}
return {**target, **siblings} if isinstance(target, dict) else siblings
if "propertyName" in node and "mapping" in node:
return {"propertyName": node["propertyName"]}
return {key: _inlined(value, defs, depth + 1) for key, value in node.items() if key != "$defs"}
def inline_request_body(model_class: type, example: Mapping[str, object]) -> JsonObject:
"""
Build an ``openapi_extra["requestBody"]`` for a route that reads its body with ``request.body()``.
Swagger UI resolves ``$ref`` against the whole document, so a schema placed inline in an operation
cannot keep Pydantic's ``#/$defs/...`` references. Every reference is expanded in place instead,
which also keeps the operation self-contained inside the lazy OpenAPI snapshot.
"""
schema: Final = CustomOpenAPISpec.get_pydantic_schema(model_class)
media: Final[JsonObject] = {"example": cast(JsonValue, dict(example))} # cast-ok: JSON example literal
if schema is None:
return {"required": True, "content": {"application/json": media}}
raw_defs: Final = schema.get("$defs")
defs: Final[Mapping[str, JsonValue]] = raw_defs if isinstance(raw_defs, dict) else MappingProxyType({})
return {
"required": True,
"content": {"application/json": {"schema": _inlined(schema, defs), **media}},
}

View file

@ -9,7 +9,15 @@ from pydantic import TypeAdapter, ValidationError
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, OpenAIDecisionRequestBody
from litellm.proxy.common_utils.custom_openapi_spec import inline_request_body
from litellm.types.decisions import (
DecisionsRequest,
DecisionsRequestBody,
DecisionsResponse,
OpenAIDecisionRequest,
OpenAIDecisionRequestBody,
OpenAIDecisionResponse,
)
router: Final = APIRouter()
_REQUEST_DATA_ADAPTER: Final[TypeAdapter[dict[str, object]]] = TypeAdapter(dict[str, object])
@ -20,6 +28,51 @@ _OPENAI_DECISION_REQUEST_BODY_ADAPTER: Final[TypeAdapter[OpenAIDecisionRequestBo
_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)
_SYSTEMONE_REQUEST_BODY: Final = inline_request_body(
DecisionsRequest,
{
"model": "jev",
"state": "Customer wrote: I was charged twice for order #4411 and want one charge refunded today.",
"questions": {
"is_refund_request": {"type": "noul", "instructions": "Is the customer asking for a refund?"},
"urgency": {
"type": "choice",
"instructions": "How urgent is this?",
"criteria": {"low": "can wait a week", "high": "needs action today"},
},
"frustration": {
"type": "score",
"instructions": "How frustrated is the customer?",
"criteria": ["calm", "mildly annoyed", "angry"],
},
},
},
)
_DECISIONS_REQUEST_BODY: Final = inline_request_body(
OpenAIDecisionRequest,
{
"model": "luna",
"input": "Customer wrote: I was charged twice for order #4411 and want one charge refunded today.",
"questions": [
{"type": "predicate", "name": "is_refund_request", "instructions": "Is the customer asking for a refund?"},
{
"type": "choice",
"name": "urgency",
"instructions": "How urgent is this?",
"choices": [
{"value": "low", "description": "can wait a week"},
{"value": "high", "description": "needs action today"},
],
},
{
"type": "score",
"name": "frustration",
"instructions": "How frustrated is the customer?",
"levels": [{"label": "calm"}, {"label": "mildly annoyed"}, {"label": "angry"}],
},
],
},
)
async def _invalid_request(
@ -121,18 +174,35 @@ async def _process_decisions(
dependencies=[Depends(user_api_key_auth)],
response_class=ORJSONResponse, # pyright: ignore[reportDeprecated] # required endpoint contract
tags=["decisions"],
summary="Ask named questions about a state (System One format)",
responses={200: {"model": DecisionsResponse}},
openapi_extra={"requestBody": _SYSTEMONE_REQUEST_BODY},
)
@router.post(
"/systemone",
dependencies=[Depends(user_api_key_auth)],
response_class=ORJSONResponse, # pyright: ignore[reportDeprecated] # required endpoint contract
tags=["decisions"],
summary="Ask named questions about a state (System One format)",
responses={200: {"model": DecisionsResponse}},
openapi_extra={"requestBody": _SYSTEMONE_REQUEST_BODY},
)
async def systemone(
request: Request,
fastapi_response: Response,
user_api_key_dict: Annotated[UserAPIKeyAuth, Depends(user_api_key_auth)],
):
"""
Judge a `state` against 1 to 128 named `questions` and get one calibrated answer per question name.
Question types are `noul` (probability the answer is yes), `choice` (one label out of `criteria`, with
`confidence` and `probabilities`) and `score` (an index into the `criteria` levels, with `confidence`,
`legend` and `probabilities`). Requests that break these rules are rejected with a 400 before any provider
is called. `model` is any decision model in the proxy model list. OpenAI decision models accept this format
too, LiteLLM translates it and keeps the answers keyed by question name. Streaming is not supported.
[Docs](https://docs.litellm.ai/docs/decisions)
"""
return await _process_decisions(
request=request,
fastapi_response=fastapi_response,
@ -146,18 +216,35 @@ async def systemone(
dependencies=[Depends(user_api_key_auth)],
response_class=ORJSONResponse, # pyright: ignore[reportDeprecated] # required endpoint contract
tags=["decisions"],
summary="Ask questions about an input (OpenAI Decisions format)",
responses={200: {"model": OpenAIDecisionResponse}},
openapi_extra={"requestBody": _DECISIONS_REQUEST_BODY},
)
@router.post(
"/decisions",
dependencies=[Depends(user_api_key_auth)],
response_class=ORJSONResponse, # pyright: ignore[reportDeprecated] # required endpoint contract
tags=["decisions"],
summary="Ask questions about an input (OpenAI Decisions format)",
responses={200: {"model": OpenAIDecisionResponse}},
openapi_extra={"requestBody": _DECISIONS_REQUEST_BODY},
)
async def decisions(
request: Request,
fastapi_response: Response,
user_api_key_dict: Annotated[UserAPIKeyAuth, Depends(user_api_key_auth)],
):
"""
Judge an `input` against 1 to 128 `questions` and get the answers back in the same order.
Question types are `predicate` (yes/no with `probability`), `choice` (one of the `choices`, with
`probabilities`) and `score` (an index into `levels`, with `probabilities`). Requests that break these
rules are rejected with a 400 before any provider is called. `model` is any decision model in the proxy
model list. System One decision models accept this format too, LiteLLM translates the request and the
answers. Streaming is not supported.
[Docs](https://docs.litellm.ai/docs/decisions)
"""
return await _process_decisions(
request=request,
fastapi_response=fastapi_response,

View file

@ -15,8 +15,13 @@ MAX_DECISION_QUESTIONS: Final = 128
class NoulQuestion(LiteLLMPydanticObjectBase):
type: Literal["noul"]
instructions: DecisionsJSON | None = None
criteria: NoulCriteria | None = None
instructions: Annotated[DecisionsJSON | None, Field(description="The yes/no question to ask about the state")] = (
None
)
criteria: Annotated[
NoulCriteria | None,
Field(description="Optional descriptions of what makes the answer true and what makes it false"),
] = None
model_config = ConfigDict(extra="allow", frozen=True)
@ -29,16 +34,24 @@ class NoulQuestion(LiteLLMPydanticObjectBase):
class ChoiceQuestion(LiteLLMPydanticObjectBase):
type: Literal["choice"]
instructions: DecisionsJSON | None = None
criteria: Annotated[Mapping[str, DecisionsJSON | None], Field(min_length=1, max_length=255)]
instructions: Annotated[DecisionsJSON | None, Field(description="The question to ask about the state")] = None
criteria: Annotated[
Mapping[str, DecisionsJSON | None],
Field(min_length=1, max_length=255, description="Candidate labels mapped to an optional description of each"),
]
model_config = ConfigDict(extra="allow", frozen=True)
class ScoreQuestion(LiteLLMPydanticObjectBase):
type: Literal["score"]
instructions: DecisionsJSON | None = None
criteria: Annotated[Sequence[DecisionsJSON], Field(min_length=1, max_length=10)]
instructions: Annotated[DecisionsJSON | None, Field(description="The question to ask about the state")] = None
criteria: Annotated[
Sequence[DecisionsJSON],
Field(
min_length=1, max_length=10, description="Scale levels from lowest to highest. The score is a level index"
),
]
model_config = ConfigDict(extra="allow", frozen=True)
@ -55,14 +68,20 @@ DecisionQuestionMap: TypeAlias = Annotated[
class DecisionsRequestBody(LiteLLMPydanticObjectBase):
state: DecisionsJSON
questions: DecisionQuestionMap
state: Annotated[
DecisionsJSON,
Field(description="The thing being judged, such as a support ticket, a document or a chat transcript"),
]
questions: Annotated[
DecisionQuestionMap,
Field(description="Named noul, choice and score questions. Each key becomes a key in the answers"),
]
model_config = ConfigDict(extra="allow", frozen=True)
class DecisionsRequest(DecisionsRequestBody):
model: str
model: Annotated[str, Field(description="A decision model from the proxy model_list")]
@with_config(ConfigDict(extra="allow"))
@ -171,8 +190,8 @@ OpenAIDecisionInput: TypeAlias = str | Sequence[OpenAIDecisionInputMessage]
class OpenAIPredicateQuestion(LiteLLMPydanticObjectBase):
type: Literal["predicate"]
name: str | None = None
instructions: str
name: Annotated[str | None, Field(description="Echoed in the matching answer")] = None
instructions: Annotated[str, Field(description="The yes/no question to ask about the input")]
model_config = ConfigDict(extra="forbid", frozen=True)
@ -192,9 +211,12 @@ def systemone_choice_key(value: str | bool) -> str:
class OpenAIChoiceQuestion(LiteLLMPydanticObjectBase):
type: Literal["choice"]
name: str | None = None
instructions: str
choices: Annotated[Sequence[OpenAIChoiceOption], Field(min_length=2, max_length=255)]
name: Annotated[str | None, Field(description="Echoed in the matching answer")] = None
instructions: Annotated[str, Field(description="The question to ask about the input")]
choices: Annotated[
Sequence[OpenAIChoiceOption],
Field(min_length=2, max_length=255, description="The options the model picks from. Values must be unique"),
]
model_config = ConfigDict(extra="forbid", frozen=True)
@ -215,9 +237,14 @@ class OpenAIScoreLevel(LiteLLMPydanticObjectBase):
class OpenAIScoreQuestion(LiteLLMPydanticObjectBase):
type: Literal["score"]
name: str | None = None
instructions: str
levels: Annotated[Sequence[OpenAIScoreLevel], Field(min_length=2, max_length=10)]
name: Annotated[str | None, Field(description="Echoed in the matching answer")] = None
instructions: Annotated[str, Field(description="The question to ask about the input")]
levels: Annotated[
Sequence[OpenAIScoreLevel],
Field(
min_length=2, max_length=10, description="Scale levels from lowest to highest. The score is a level index"
),
]
model_config = ConfigDict(extra="forbid", frozen=True)
@ -229,13 +256,32 @@ OpenAIDecisionQuestion: TypeAlias = Annotated[
class OpenAIDecisionRequestBody(LiteLLMPydanticObjectBase):
input: OpenAIDecisionInput
questions: Annotated[Sequence[OpenAIDecisionQuestion], Field(min_length=1, max_length=MAX_DECISION_QUESTIONS)]
safety_identifier: str | None = None
input: Annotated[
OpenAIDecisionInput,
Field(
description="The text being judged, or user messages whose content mixes input_text and input_image parts"
),
]
questions: Annotated[
Sequence[OpenAIDecisionQuestion],
Field(
min_length=1,
max_length=MAX_DECISION_QUESTIONS,
description="Predicate, choice and score questions. Answers come back in the same order",
),
]
safety_identifier: Annotated[
str | None,
Field(description="A stable id for your end user. Sent to OpenAI and dropped for other providers"),
] = None
model_config = ConfigDict(extra="allow", frozen=True)
class OpenAIDecisionRequest(OpenAIDecisionRequestBody):
model: Annotated[str, Field(description="A decision model from the proxy model_list")]
class OpenAIPredicateAnswer(LiteLLMPydanticObjectBase):
type: Literal["predicate"] = "predicate"
name: str | None

View file

@ -58,6 +58,7 @@ IGNORE_FUNCTIONS = [
"sanitize_oci_schema", # OCI: bounded by JSON-schema tree depth.
"_freeze_for_dedupe", # OTEL: max depth set (default 16, _FREEZE_MAX_DEPTH); fails closed by returning repr(value) at the cap.
"apply_json_merge_patch", # max depth set (_MAX_MERGE_DEPTH=64); fails closed by raising ValueError at the cap.
"_inlined", # max depth set (_MAX_INLINE_DEPTH=32); passes the schema node through untouched at the cap. Walks a Pydantic JSON schema once at import time.
"_filter_argument_value", # max depth set (DEFAULT_MAX_RECURSE_DEPTH); fails closed by blocking the tool call at the cap.
"_redact_scanned_content", # max depth set (DEFAULT_MAX_RECURSE_DEPTH); fails closed by returning "[REDACTED]" at the cap.
"replace_ciphertexts", # max depth set (DEFAULT_MAX_RECURSE_DEPTH); walks stored JSON, which has no cycles, and leaves values below the cap untouched.

View file

@ -4,11 +4,14 @@ Simple unit tests for CustomOpenAPISpec class.
Tests basic functionality of OpenAPI schema generation.
"""
from unittest.mock import Mock, patch
import json
from typing import Annotated, Literal
from unittest.mock import patch
import pytest
from pydantic import BaseModel, ConfigDict, Field
from litellm.proxy.common_utils.custom_openapi_spec import CustomOpenAPISpec
from litellm.proxy.common_utils.custom_openapi_spec import CustomOpenAPISpec, inline_request_body
class TestCustomOpenAPISpec:
@ -27,19 +30,13 @@ class TestCustomOpenAPISpec:
},
}
@patch(
"litellm.proxy.common_utils.custom_openapi_spec.CustomOpenAPISpec.add_request_schema"
)
def test_add_chat_completion_request_schema(
self, mock_add_schema, base_openapi_schema
):
@patch("litellm.proxy.common_utils.custom_openapi_spec.CustomOpenAPISpec.add_request_schema")
def test_add_chat_completion_request_schema(self, mock_add_schema, base_openapi_schema):
"""Test that chat completion schema is added correctly."""
mock_add_schema.return_value = base_openapi_schema
with patch("litellm.proxy._types.ProxyChatCompletionRequest") as mock_model:
result = CustomOpenAPISpec.add_chat_completion_request_schema(
base_openapi_schema
)
result = CustomOpenAPISpec.add_chat_completion_request_schema(base_openapi_schema)
mock_add_schema.assert_called_once_with(
openapi_schema=base_openapi_schema,
@ -50,9 +47,7 @@ class TestCustomOpenAPISpec:
)
assert result == base_openapi_schema
@patch(
"litellm.proxy.common_utils.custom_openapi_spec.CustomOpenAPISpec.add_request_schema"
)
@patch("litellm.proxy.common_utils.custom_openapi_spec.CustomOpenAPISpec.add_request_schema")
def test_add_embedding_request_schema(self, mock_add_schema, base_openapi_schema):
"""Test that embedding schema is added correctly."""
mock_add_schema.return_value = base_openapi_schema
@ -69,19 +64,13 @@ class TestCustomOpenAPISpec:
)
assert result == base_openapi_schema
@patch(
"litellm.proxy.common_utils.custom_openapi_spec.CustomOpenAPISpec.add_request_schema"
)
def test_add_responses_api_request_schema(
self, mock_add_schema, base_openapi_schema
):
@patch("litellm.proxy.common_utils.custom_openapi_spec.CustomOpenAPISpec.add_request_schema")
def test_add_responses_api_request_schema(self, mock_add_schema, base_openapi_schema):
"""Test that responses API schema is added correctly."""
mock_add_schema.return_value = base_openapi_schema
with patch("litellm.types.llms.openai.ResponsesAPIRequestParams") as mock_model:
result = CustomOpenAPISpec.add_responses_api_request_schema(
base_openapi_schema
)
result = CustomOpenAPISpec.add_responses_api_request_schema(base_openapi_schema)
mock_add_schema.assert_called_once_with(
openapi_schema=base_openapi_schema,
@ -123,15 +112,11 @@ def test_defs_rewritten_in_add_schema_to_components():
)
assert "$defs" not in openapi_schema
assert (
openapi_schema["components"]["schemas"]["SchemaName"]["properties"]["messages"][
"items"
]["anyOf"][0]["$ref"]
openapi_schema["components"]["schemas"]["SchemaName"]["properties"]["messages"]["items"]["anyOf"][0]["$ref"]
== "#/components/schemas/UserMessage"
)
assert (
openapi_schema["components"]["schemas"]["SchemaName"]["properties"]["messages"][
"items"
]["anyOf"][1]["$ref"]
openapi_schema["components"]["schemas"]["SchemaName"]["properties"]["messages"]["items"]["anyOf"][1]["$ref"]
== "#/components/schemas/AssistantMessage"
)
@ -188,14 +173,8 @@ def test_rewrite_defs_refs():
rewritten = CustomOpenAPISpec._rewrite_defs_refs(schema=schema, renames={})
assert "$defs" not in rewritten
assert (
rewritten["properties"]["messages"]["items"]["anyOf"][0]["$ref"]
== "#/components/schemas/UserMessage"
)
assert (
rewritten["properties"]["messages"]["items"]["anyOf"][1]["$ref"]
== "#/components/schemas/AssistantMessage"
)
assert rewritten["properties"]["messages"]["items"]["anyOf"][0]["$ref"] == "#/components/schemas/UserMessage"
assert rewritten["properties"]["messages"]["items"]["anyOf"][1]["$ref"] == "#/components/schemas/AssistantMessage"
def test_get_pydantic_schema_generates_schema_for_responses_request_typed_dict():
@ -390,3 +369,42 @@ def test_add_schema_to_components_renames_def_with_different_required_set():
assert schemas["Block"]["required"] == ["type", "x", "y"]
assert schemas["Req_Block"]["required"] == ["keys", "type", "x", "y"]
assert schemas["Req"]["properties"]["b"]["$ref"] == "#/components/schemas/Req_Block"
class _Cat(BaseModel):
kind: Literal["cat"]
lives: int = 9
model_config = ConfigDict(frozen=True)
class _Dog(BaseModel):
kind: Literal["dog"]
good: bool = True
model_config = ConfigDict(frozen=True)
class _Shelter(BaseModel):
pets: dict[str, Annotated[_Cat | _Dog, Field(discriminator="kind")]]
favorite: _Cat | None = None
model_config = ConfigDict(frozen=True)
def test_inline_request_body_expands_every_ref_so_the_operation_stands_alone():
body = inline_request_body(_Shelter, {"pets": {"tom": {"kind": "cat"}}})
media = body["content"]["application/json"]
assert body["required"] is True
assert media["example"] == {"pets": {"tom": {"kind": "cat"}}}
assert "$ref" not in json.dumps(media["schema"])
assert "$defs" not in media["schema"]
pet_schema = media["schema"]["properties"]["pets"]["additionalProperties"]
variants = {variant["properties"]["kind"]["const"]: variant for variant in pet_schema["oneOf"]}
assert variants["cat"]["properties"]["lives"]["default"] == 9
assert variants["dog"]["properties"]["good"]["type"] == "boolean"
assert pet_schema["discriminator"] == {"propertyName": "kind"}
favorite = media["schema"]["properties"]["favorite"]
assert favorite["default"] is None
assert any(option.get("properties", {}).get("lives") for option in favorite["anyOf"])

View file

@ -12,17 +12,19 @@ import respx
from fastapi import FastAPI
from fastapi.routing import APIRoute
from fastapi.testclient import TestClient
from pydantic import TypeAdapter
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, systemone
from litellm.proxy.decisions_endpoints.endpoints import decisions, router, systemone
from litellm.proxy.pass_through_endpoints.pass_through_endpoints import SafeRouteAdder
from litellm.proxy.proxy_server import (
app,
cleanup_router_config_variables,
initialize,
)
from litellm.types.decisions import DecisionsRequestBody, OpenAIDecisionRequestBody
_INPUT_TOKENS: Final[int] = 367
_OUTPUT_TOKENS: Final[int] = 3
@ -688,3 +690,63 @@ def test_with_lazy_routes_disabled_a_config_pass_through_at_v1_decisions_still_w
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
@pytest.fixture(scope="module")
def decisions_openapi() -> dict[str, object]:
app = FastAPI()
app.include_router(router)
return app.openapi()
@pytest.mark.parametrize(
("path", "body_adapter", "response_schema", "required"),
(
("/v1/systemone", TypeAdapter(DecisionsRequestBody), "DecisionsResponse", {"model", "state", "questions"}),
("/systemone", TypeAdapter(DecisionsRequestBody), "DecisionsResponse", {"model", "state", "questions"}),
(
"/v1/decisions",
TypeAdapter(OpenAIDecisionRequestBody),
"OpenAIDecisionResponse",
{"model", "input", "questions"},
),
(
"/decisions",
TypeAdapter(OpenAIDecisionRequestBody),
"OpenAIDecisionResponse",
{"model", "input", "questions"},
),
),
)
def test_swagger_documents_the_request_format_and_response_of_each_decisions_route(
decisions_openapi: dict[str, object],
path: str,
body_adapter: TypeAdapter[DecisionsRequestBody] | TypeAdapter[OpenAIDecisionRequestBody],
response_schema: str,
required: set[str],
) -> None:
operation = decisions_openapi["paths"][path]["post"]
media = operation["requestBody"]["content"]["application/json"]
assert "Decisions" in operation["summary"] or "System One" in operation["summary"]
assert "docs.litellm.ai/docs/decisions" in operation["description"]
assert required <= set(media["schema"]["required"])
assert required <= set(media["schema"]["properties"])
assert "$ref" not in json.dumps(media["schema"])
assert media["schema"]["properties"]["model"]["description"]
example = {key: value for key, value in media["example"].items() if key != "model"}
assert len(body_adapter.validate_python(example).questions) == 3
response_ref = operation["responses"]["200"]["content"]["application/json"]["schema"]["$ref"]
assert response_ref == f"#/components/schemas/{response_schema}"
assert "answers" in decisions_openapi["components"]["schemas"][response_schema]["properties"]
def test_the_two_decisions_routes_document_different_request_formats(decisions_openapi: dict[str, object]) -> None:
def properties(path: str) -> set[str]:
operation = decisions_openapi["paths"][path]["post"]
return set(operation["requestBody"]["content"]["application/json"]["schema"]["properties"])
assert "state" in properties("/v1/systemone") and "state" not in properties("/v1/decisions")
assert "input" in properties("/v1/decisions") and "input" not in properties("/v1/systemone")

View file

@ -4703,7 +4703,18 @@ export interface paths {
};
get?: never;
put?: never;
/** Decisions */
/**
* Ask questions about an input (OpenAI Decisions format)
* @description Judge an `input` against 1 to 128 `questions` and get the answers back in the same order.
*
* Question types are `predicate` (yes/no with `probability`), `choice` (one of the `choices`, with
* `probabilities`) and `score` (an index into `levels`, with `probabilities`). Requests that break these
* rules are rejected with a 400 before any provider is called. `model` is any decision model in the proxy
* model list. System One decision models accept this format too, LiteLLM translates the request and the
* answers. Streaming is not supported.
*
* [Docs](https://docs.litellm.ai/docs/decisions)
*/
post: operations["decisions_decisions_post"];
delete?: never;
options?: never;
@ -15837,7 +15848,18 @@ export interface paths {
};
get?: never;
put?: never;
/** Systemone */
/**
* Ask named questions about a state (System One format)
* @description Judge a `state` against 1 to 128 named `questions` and get one calibrated answer per question name.
*
* Question types are `noul` (probability the answer is yes), `choice` (one label out of `criteria`, with
* `confidence` and `probabilities`) and `score` (an index into the `criteria` levels, with `confidence`,
* `legend` and `probabilities`). Requests that break these rules are rejected with a 400 before any provider
* is called. `model` is any decision model in the proxy model list. OpenAI decision models accept this format
* too, LiteLLM translates it and keeps the answers keyed by question name. Streaming is not supported.
*
* [Docs](https://docs.litellm.ai/docs/decisions)
*/
post: operations["systemone_systemone_post"];
delete?: never;
options?: never;
@ -19646,7 +19668,18 @@ export interface paths {
};
get?: never;
put?: never;
/** Decisions */
/**
* Ask questions about an input (OpenAI Decisions format)
* @description Judge an `input` against 1 to 128 `questions` and get the answers back in the same order.
*
* Question types are `predicate` (yes/no with `probability`), `choice` (one of the `choices`, with
* `probabilities`) and `score` (an index into `levels`, with `probabilities`). Requests that break these
* rules are rejected with a 400 before any provider is called. `model` is any decision model in the proxy
* model list. System One decision models accept this format too, LiteLLM translates the request and the
* answers. Streaming is not supported.
*
* [Docs](https://docs.litellm.ai/docs/decisions)
*/
post: operations["decisions_v1_decisions_post"];
delete?: never;
options?: never;
@ -21991,7 +22024,18 @@ export interface paths {
};
get?: never;
put?: never;
/** Systemone */
/**
* Ask named questions about a state (System One format)
* @description Judge a `state` against 1 to 128 named `questions` and get one calibrated answer per question name.
*
* Question types are `noul` (probability the answer is yes), `choice` (one label out of `criteria`, with
* `confidence` and `probabilities`) and `score` (an index into the `criteria` levels, with `confidence`,
* `legend` and `probabilities`). Requests that break these rules are rejected with a 400 before any provider
* is called. `model` is any decision model in the proxy model list. OpenAI decision models accept this format
* too, LiteLLM translates it and keeps the answers keyed by question name. Streaming is not supported.
*
* [Docs](https://docs.litellm.ai/docs/decisions)
*/
post: operations["systemone_v1_systemone_post"];
delete?: never;
options?: never;
@ -28642,6 +28686,24 @@ export interface components {
*/
role: "user" | "assistant";
};
/** ChoiceAnswer */
ChoiceAnswer: {
/** Choice */
choice: string;
/** Confidence */
confidence: number;
/** Probabilities */
probabilities: {
[key: string]: number;
};
/**
* @description discriminator enum property added by openapi-typescript
* @enum {string}
*/
type: "choice";
} & {
[key: string]: unknown;
};
/** ChoiceLogprobs */
ChoiceLogprobs: {
/** Content */
@ -31008,6 +31070,33 @@ export interface components {
*/
total_tokens: number;
};
/** DecisionsResponse */
DecisionsResponse: {
/** Answers */
answers: {
[key: string]: components["schemas"]["NoulAnswer"] | components["schemas"]["ChoiceAnswer"] | components["schemas"]["ScoreAnswer"];
};
/** Model */
model?: string | null;
usage?: components["schemas"]["DecisionsUsage"] | null;
} & {
[key: string]: unknown;
};
/** DecisionsUsage */
DecisionsUsage: {
/**
* Input Tokens
* @default 0
*/
input_tokens: number;
/**
* Output Tokens
* @default 0
*/
output_tokens: number;
} & {
[key: string]: unknown;
};
/**
* DefaultInternalUserParams
* @description Default parameters to apply when a new user signs in via SSO or is created on the /user/new API endpoint
@ -38834,6 +38923,18 @@ export interface components {
/** User Role */
user_role?: ("proxy_admin" | "proxy_admin_viewer" | "internal_user" | "internal_user_viewer") | null;
};
/** NoulAnswer */
NoulAnswer: {
/** Noul */
noul: number;
/**
* @description discriminator enum property added by openapi-typescript
* @enum {string}
*/
type: "noul";
} & {
[key: string]: unknown;
};
/**
* OAuth2SecurityScheme
* @description Defines a security scheme using OAuth 2.0.
@ -38872,6 +38973,129 @@ export interface components {
[key: string]: unknown;
} | null;
};
/** OpenAIChoiceAnswer */
OpenAIChoiceAnswer: {
/** Choice */
choice: string | boolean;
/** Confidence */
confidence: number;
/** Name */
name: string | null;
/** Probabilities */
probabilities: components["schemas"]["OpenAIChoiceProbability"][];
/**
* @description discriminator enum property added by openapi-typescript
* @enum {string}
*/
type: "choice";
};
/** OpenAIChoiceProbability */
OpenAIChoiceProbability: {
/** Probability */
probability: number;
/** Value */
value: string | boolean;
};
/** OpenAIDecisionInputTokensDetails */
OpenAIDecisionInputTokensDetails: {
/**
* Cache Write Tokens
* @default 0
*/
cache_write_tokens: number;
/**
* Cached Tokens
* @default 0
*/
cached_tokens: number;
};
/** OpenAIDecisionOutputTokensDetails */
OpenAIDecisionOutputTokensDetails: {
/**
* Reasoning Tokens
* @default 0
*/
reasoning_tokens: number;
};
/** OpenAIDecisionResponse */
OpenAIDecisionResponse: {
/** Answers */
answers: (components["schemas"]["OpenAIPredicateAnswer"] | components["schemas"]["OpenAIChoiceAnswer"] | components["schemas"]["OpenAIScoreAnswer"] | components["schemas"]["OpenAIRefusalAnswer"])[];
/** Model */
model: string;
usage: components["schemas"]["OpenAIDecisionUsage"];
} & {
[key: string]: unknown;
};
/** OpenAIDecisionUsage */
OpenAIDecisionUsage: {
/** Input Tokens */
input_tokens: number;
/**
* @default {
* "cache_write_tokens": 0,
* "cached_tokens": 0
* }
*/
input_tokens_details: components["schemas"]["OpenAIDecisionInputTokensDetails"];
/** Output Tokens */
output_tokens: number;
/**
* @default {
* "reasoning_tokens": 0
* }
*/
output_tokens_details: components["schemas"]["OpenAIDecisionOutputTokensDetails"];
/** Total Tokens */
total_tokens: number;
};
/** OpenAIPredicateAnswer */
OpenAIPredicateAnswer: {
/** Name */
name: string | null;
/** Probability */
probability: number;
/**
* @description discriminator enum property added by openapi-typescript
* @enum {string}
*/
type: "predicate";
};
/** OpenAIRefusalAnswer */
OpenAIRefusalAnswer: {
/** Name */
name: string | null;
/**
* @description discriminator enum property added by openapi-typescript
* @enum {string}
*/
type: "refusal";
};
/** OpenAIScoreAnswer */
OpenAIScoreAnswer: {
/** Confidence */
confidence: number;
/** Name */
name: string | null;
/** Probabilities */
probabilities: components["schemas"]["OpenAIScoreProbability"][];
/** Score */
score: number;
/**
* @description discriminator enum property added by openapi-typescript
* @enum {string}
*/
type: "score";
};
/** OpenAIScoreProbability */
OpenAIScoreProbability: {
/** Label */
label: string;
/** Probability */
probability: number;
/** Value */
value: number;
};
/**
* OpenIdConnectSecurityScheme
* @description Defines a security scheme using OpenID Connect.
@ -43986,6 +44210,30 @@ export interface components {
*/
window_seconds: number;
};
/** ScoreAnswer */
ScoreAnswer: {
/** Confidence */
confidence: number;
/** Legend */
legend: {
[key: string]: string | {
[key: string]: unknown;
} | unknown[];
};
/** Probabilities */
probabilities: {
[key: string]: number;
};
/** Score */
score: number;
/**
* @description discriminator enum property added by openapi-typescript
* @enum {string}
*/
type: "score";
} & {
[key: string]: unknown;
};
/**
* Screenshot
* @description A screenshot action.
@ -56556,7 +56804,179 @@ export interface operations {
path?: never;
cookie?: never;
};
requestBody?: never;
requestBody: {
content: {
/**
* @example {
* "input": "Customer wrote: I was charged twice for order #4411 and want one charge refunded today.",
* "model": "luna",
* "questions": [
* {
* "instructions": "Is the customer asking for a refund?",
* "name": "is_refund_request",
* "type": "predicate"
* },
* {
* "choices": [
* {
* "description": "can wait a week",
* "value": "low"
* },
* {
* "description": "needs action today",
* "value": "high"
* }
* ],
* "instructions": "How urgent is this?",
* "name": "urgency",
* "type": "choice"
* },
* {
* "instructions": "How frustrated is the customer?",
* "levels": [
* {
* "label": "calm"
* },
* {
* "label": "mildly annoyed"
* },
* {
* "label": "angry"
* }
* ],
* "name": "frustration",
* "type": "score"
* }
* ]
* }
*/
"application/json": {
/**
* Input
* @description The text being judged, or user messages whose content mixes input_text and input_image parts
*/
input: string | {
/** Content */
content: string | ({
/** Text */
text: string;
/**
* Type
* @constant
*/
type: "input_text";
} | {
/** Detail */
detail?: string | null;
/** Image Url */
image_url: string;
/**
* Type
* @constant
*/
type: "input_image";
})[];
/**
* Role
* @default user
* @constant
*/
role?: "user";
/**
* Type
* @default message
* @constant
*/
type?: "message";
}[];
/**
* Model
* @description A decision model from the proxy model_list
*/
model: string;
/**
* Questions
* @description Predicate, choice and score questions. Answers come back in the same order
*/
questions: ({
/**
* Instructions
* @description The yes/no question to ask about the input
*/
instructions: string;
/**
* Name
* @description Echoed in the matching answer
*/
name?: string | null;
/**
* Type
* @constant
*/
type: "predicate";
} | {
/**
* Choices
* @description The options the model picks from. Values must be unique
*/
choices: {
/** Description */
description?: string | null;
/** Value */
value: string | boolean;
}[];
/**
* Instructions
* @description The question to ask about the input
*/
instructions: string;
/**
* Name
* @description Echoed in the matching answer
*/
name?: string | null;
/**
* Type
* @constant
*/
type: "choice";
} | {
/**
* Instructions
* @description The question to ask about the input
*/
instructions: string;
/**
* Levels
* @description Scale levels from lowest to highest. The score is a level index
*/
levels: {
/** Description */
description?: string | null;
/** Label */
label: string;
}[];
/**
* Name
* @description Echoed in the matching answer
*/
name?: string | null;
/**
* Type
* @constant
*/
type: "score";
})[];
/**
* Safety Identifier
* @description A stable id for your end user. Sent to OpenAI and dropped for other providers
*/
safety_identifier?: string | null;
} & {
[key: string]: unknown;
};
};
};
responses: {
/** @description Successful Response */
200: {
@ -56564,7 +56984,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/json": unknown;
"application/json": components["schemas"]["OpenAIDecisionResponse"];
};
};
};
@ -70673,7 +71093,132 @@ export interface operations {
path?: never;
cookie?: never;
};
requestBody?: never;
requestBody: {
content: {
/**
* @example {
* "model": "jev",
* "questions": {
* "frustration": {
* "criteria": [
* "calm",
* "mildly annoyed",
* "angry"
* ],
* "instructions": "How frustrated is the customer?",
* "type": "score"
* },
* "is_refund_request": {
* "instructions": "Is the customer asking for a refund?",
* "type": "noul"
* },
* "urgency": {
* "criteria": {
* "high": "needs action today",
* "low": "can wait a week"
* },
* "instructions": "How urgent is this?",
* "type": "choice"
* }
* },
* "state": "Customer wrote: I was charged twice for order #4411 and want one charge refunded today."
* }
*/
"application/json": {
/**
* Model
* @description A decision model from the proxy model_list
*/
model: string;
/**
* Questions
* @description Named noul, choice and score questions. Each key becomes a key in the answers
*/
questions: {
[key: string]: ({
/**
* Criteria
* @description Optional descriptions of what makes the answer true and what makes it false
*/
criteria?: {
[key: string]: string | {
[key: string]: unknown;
} | unknown[] | null;
} | null;
/**
* Instructions
* @description The yes/no question to ask about the state
*/
instructions?: string | {
[key: string]: unknown;
} | unknown[] | null;
/**
* Type
* @constant
*/
type: "noul";
} & {
[key: string]: unknown;
}) | ({
/**
* Criteria
* @description Candidate labels mapped to an optional description of each
*/
criteria: {
[key: string]: string | {
[key: string]: unknown;
} | unknown[] | null;
};
/**
* Instructions
* @description The question to ask about the state
*/
instructions?: string | {
[key: string]: unknown;
} | unknown[] | null;
/**
* Type
* @constant
*/
type: "choice";
} & {
[key: string]: unknown;
}) | ({
/**
* Criteria
* @description Scale levels from lowest to highest. The score is a level index
*/
criteria: (string | {
[key: string]: unknown;
} | unknown[])[];
/**
* Instructions
* @description The question to ask about the state
*/
instructions?: string | {
[key: string]: unknown;
} | unknown[] | null;
/**
* Type
* @constant
*/
type: "score";
} & {
[key: string]: unknown;
});
};
/**
* State
* @description The thing being judged, such as a support ticket, a document or a chat transcript
*/
state: string | {
[key: string]: unknown;
} | unknown[];
} & {
[key: string]: unknown;
};
};
};
responses: {
/** @description Successful Response */
200: {
@ -70681,7 +71226,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/json": unknown;
"application/json": components["schemas"]["DecisionsResponse"];
};
};
};
@ -75840,7 +76385,179 @@ export interface operations {
path?: never;
cookie?: never;
};
requestBody?: never;
requestBody: {
content: {
/**
* @example {
* "input": "Customer wrote: I was charged twice for order #4411 and want one charge refunded today.",
* "model": "luna",
* "questions": [
* {
* "instructions": "Is the customer asking for a refund?",
* "name": "is_refund_request",
* "type": "predicate"
* },
* {
* "choices": [
* {
* "description": "can wait a week",
* "value": "low"
* },
* {
* "description": "needs action today",
* "value": "high"
* }
* ],
* "instructions": "How urgent is this?",
* "name": "urgency",
* "type": "choice"
* },
* {
* "instructions": "How frustrated is the customer?",
* "levels": [
* {
* "label": "calm"
* },
* {
* "label": "mildly annoyed"
* },
* {
* "label": "angry"
* }
* ],
* "name": "frustration",
* "type": "score"
* }
* ]
* }
*/
"application/json": {
/**
* Input
* @description The text being judged, or user messages whose content mixes input_text and input_image parts
*/
input: string | {
/** Content */
content: string | ({
/** Text */
text: string;
/**
* Type
* @constant
*/
type: "input_text";
} | {
/** Detail */
detail?: string | null;
/** Image Url */
image_url: string;
/**
* Type
* @constant
*/
type: "input_image";
})[];
/**
* Role
* @default user
* @constant
*/
role?: "user";
/**
* Type
* @default message
* @constant
*/
type?: "message";
}[];
/**
* Model
* @description A decision model from the proxy model_list
*/
model: string;
/**
* Questions
* @description Predicate, choice and score questions. Answers come back in the same order
*/
questions: ({
/**
* Instructions
* @description The yes/no question to ask about the input
*/
instructions: string;
/**
* Name
* @description Echoed in the matching answer
*/
name?: string | null;
/**
* Type
* @constant
*/
type: "predicate";
} | {
/**
* Choices
* @description The options the model picks from. Values must be unique
*/
choices: {
/** Description */
description?: string | null;
/** Value */
value: string | boolean;
}[];
/**
* Instructions
* @description The question to ask about the input
*/
instructions: string;
/**
* Name
* @description Echoed in the matching answer
*/
name?: string | null;
/**
* Type
* @constant
*/
type: "choice";
} | {
/**
* Instructions
* @description The question to ask about the input
*/
instructions: string;
/**
* Levels
* @description Scale levels from lowest to highest. The score is a level index
*/
levels: {
/** Description */
description?: string | null;
/** Label */
label: string;
}[];
/**
* Name
* @description Echoed in the matching answer
*/
name?: string | null;
/**
* Type
* @constant
*/
type: "score";
})[];
/**
* Safety Identifier
* @description A stable id for your end user. Sent to OpenAI and dropped for other providers
*/
safety_identifier?: string | null;
} & {
[key: string]: unknown;
};
};
};
responses: {
/** @description Successful Response */
200: {
@ -75848,7 +76565,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/json": unknown;
"application/json": components["schemas"]["OpenAIDecisionResponse"];
};
};
};
@ -79013,7 +79730,132 @@ export interface operations {
path?: never;
cookie?: never;
};
requestBody?: never;
requestBody: {
content: {
/**
* @example {
* "model": "jev",
* "questions": {
* "frustration": {
* "criteria": [
* "calm",
* "mildly annoyed",
* "angry"
* ],
* "instructions": "How frustrated is the customer?",
* "type": "score"
* },
* "is_refund_request": {
* "instructions": "Is the customer asking for a refund?",
* "type": "noul"
* },
* "urgency": {
* "criteria": {
* "high": "needs action today",
* "low": "can wait a week"
* },
* "instructions": "How urgent is this?",
* "type": "choice"
* }
* },
* "state": "Customer wrote: I was charged twice for order #4411 and want one charge refunded today."
* }
*/
"application/json": {
/**
* Model
* @description A decision model from the proxy model_list
*/
model: string;
/**
* Questions
* @description Named noul, choice and score questions. Each key becomes a key in the answers
*/
questions: {
[key: string]: ({
/**
* Criteria
* @description Optional descriptions of what makes the answer true and what makes it false
*/
criteria?: {
[key: string]: string | {
[key: string]: unknown;
} | unknown[] | null;
} | null;
/**
* Instructions
* @description The yes/no question to ask about the state
*/
instructions?: string | {
[key: string]: unknown;
} | unknown[] | null;
/**
* Type
* @constant
*/
type: "noul";
} & {
[key: string]: unknown;
}) | ({
/**
* Criteria
* @description Candidate labels mapped to an optional description of each
*/
criteria: {
[key: string]: string | {
[key: string]: unknown;
} | unknown[] | null;
};
/**
* Instructions
* @description The question to ask about the state
*/
instructions?: string | {
[key: string]: unknown;
} | unknown[] | null;
/**
* Type
* @constant
*/
type: "choice";
} & {
[key: string]: unknown;
}) | ({
/**
* Criteria
* @description Scale levels from lowest to highest. The score is a level index
*/
criteria: (string | {
[key: string]: unknown;
} | unknown[])[];
/**
* Instructions
* @description The question to ask about the state
*/
instructions?: string | {
[key: string]: unknown;
} | unknown[] | null;
/**
* Type
* @constant
*/
type: "score";
} & {
[key: string]: unknown;
});
};
/**
* State
* @description The thing being judged, such as a support ticket, a document or a chat transcript
*/
state: string | {
[key: string]: unknown;
} | unknown[];
} & {
[key: string]: unknown;
};
};
};
responses: {
/** @description Successful Response */
200: {
@ -79021,7 +79863,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/json": unknown;
"application/json": components["schemas"]["DecisionsResponse"];
};
};
};