Merge pull request #45891 from BerriAI/litellm_backport_decisions_ui_rc_1_106_0

feat(decisions): backport the rest of the decisions and System One launch to rc/1.106.0
This commit is contained in:
Mateo Wang 2026-10-10 22:15:21 -07:00 • committed by GitHub
commit 7b406378bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
125 changed files with 11911 additions and 1135 deletions

View file

@ -139,6 +139,7 @@ jobs:
test-path: |-
tests/unit/decisions
tests/unit/litellm_core_utils
tests/unit/systemone
python-version: "3.12"
workers: 4
reruns: 0
@ -343,6 +344,7 @@ jobs:
tests/unit/llms/snowflake
tests/unit/llms/soniox
tests/unit/llms/stability
tests/unit/llms/strands_decider
tests/unit/llms/tavily
tests/unit/llms/tencent
tests/unit/llms/tinyfish

View file

@ -1478,6 +1478,7 @@ from .rust_bridge import rust
from .rag.main import *
from .sandbox.main import *
from .decisions.main import *
from .systemone.main import *
from .tool_loop import ToolLoopMaxRoundsExceeded, arun_tool_loop, run_tool_loop
from .search.main import *
from .realtime_api.main import (
@ -1681,6 +1682,12 @@ if TYPE_CHECKING:
from .llms.strands_decider.decisions.transformation import (
StrandsDeciderDecisionsConfig as StrandsDeciderDecisionsConfig,
)
from .llms.databricks.decisions.transformation import (
DatabricksDecisionsConfig as DatabricksDecisionsConfig,
)
from .llms.hosted_vllm.decisions.transformation import (
HostedVLLMDecisionsConfig as HostedVLLMDecisionsConfig,
)
from .llms.openai.decisions.transformation import (
OpenAIDecisionsConfig as OpenAIDecisionsConfig,
)

View file

@ -162,6 +162,8 @@ LLM_CONFIG_NAMES: Final = (
"OpenRouterDecisionsConfig",
"CloudflareDecisionsConfig",
"StrandsDeciderDecisionsConfig",
"DatabricksDecisionsConfig",
"HostedVLLMDecisionsConfig",
"OpenAIDecisionsConfig",
"AzureAIDecisionsConfig",
"NvidiaNimRerankConfig",
@ -724,6 +726,11 @@ _LLM_CONFIGS_IMPORT_MAP: Final = {
".llms.strands_decider.decisions.transformation",
"StrandsDeciderDecisionsConfig",
),
"DatabricksDecisionsConfig": (".llms.databricks.decisions.transformation", "DatabricksDecisionsConfig"),
"HostedVLLMDecisionsConfig": (
".llms.hosted_vllm.decisions.transformation",
"HostedVLLMDecisionsConfig",
),
"OpenAIDecisionsConfig": (".llms.openai.decisions.transformation", "OpenAIDecisionsConfig"),
"AzureAIDecisionsConfig": (".llms.azure_ai.decisions.transformation", "AzureAIDecisionsConfig"),
"NvidiaNimRerankConfig": (

View file

@ -285,6 +285,7 @@ DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET: Final = int(
# Provider-specific API base URLs
XAI_API_BASE: Final = "https://api.x.ai/v1"
PLACEHOLDER_API_KEY: Final = "fake-api-key"
OPEN_SANDBOX_API_BASE_ENV_VAR: Final = "OPEN_SANDBOX_API_BASE"
OPEN_SANDBOX_API_KEY_ENV_VAR: Final = "OPEN_SANDBOX_API_KEY"
OPEN_SANDBOX_DEFAULT_TEMPLATE: Final = "opensandbox/code-interpreter:v1.1.0"

View file

@ -2026,6 +2026,8 @@ def response_cost_calculator(
"asearch",
"decisions",
"adecisions",
"systemone",
"asystemone",
],
optional_params: dict,
cache_hit: bool | None = None,

347
litellm/decisions/call.py Normal file
View file

@ -0,0 +1,347 @@
from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field, replace
from typing import Final, TypeAlias
import httpx
from pydantic import ConfigDict, TypeAdapter, ValidationError
from typing_extensions import assert_never
import litellm
from litellm.litellm_core_utils.core_helpers import RESPONSE_COST_HEADER, normalize_drop_params
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.llms.base_llm.decisions.transformation import (
BaseDecisionsConfig,
ir_to_systemone_response,
systemone_request_to_ir,
)
from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler
from litellm.llms.openai.decisions.transformation import ir_to_openai_response, openai_request_to_ir
from litellm.types.decisions import (
DecisionQuestion,
DecisionsIRRequest,
DecisionsIRResponse,
DecisionsJSON,
DecisionsRequestBody,
DecisionsResponse,
OpenAIDecisionInput,
OpenAIDecisionQuestion,
OpenAIDecisionRequestBody,
OpenAIDecisionResponse,
UnsupportedDecisionsRequest,
)
from litellm.types.utils import LlmProviders
from litellm.utils import ProviderConfigManager
SystemOneQuestions: TypeAlias = Mapping[str, DecisionQuestion | Mapping[str, object]]
OpenAIDecisionQuestions: TypeAlias = Sequence[OpenAIDecisionQuestion | Mapping[str, object]]
DecisionsRequestFormat: TypeAlias = DecisionsRequestBody | OpenAIDecisionRequestBody
_SYSTEMONE_REQUEST_ADAPTER: Final[TypeAdapter[DecisionsRequestBody]] = TypeAdapter(DecisionsRequestBody)
_OPENAI_REQUEST_ADAPTER: Final[TypeAdapter[OpenAIDecisionRequestBody]] = TypeAdapter(OpenAIDecisionRequestBody)
_SAFETY_IDENTIFIER_ADAPTER: Final[TypeAdapter[str | None]] = TypeAdapter(
str | None, config=ConfigDict(title="safety_identifier")
)
_HANDLER: Final = BaseLLMHTTPHandler()
@dataclass(frozen=True, slots=True)
class SystemOneRequestFields:
state: DecisionsJSON | None
questions: SystemOneQuestions | None
def validate(self, safety_identifier: str | None) -> DecisionsRequestBody:
return _SYSTEMONE_REQUEST_ADAPTER.validate_python({"state": self.state, "questions": self.questions})
@dataclass(frozen=True, slots=True)
class OpenAIDecisionRequestFields:
input: OpenAIDecisionInput | None
questions: OpenAIDecisionQuestions | None
def validate(self, safety_identifier: str | None) -> OpenAIDecisionRequestBody:
return _OPENAI_REQUEST_ADAPTER.validate_python(
{"input": self.input, "questions": self.questions, "safety_identifier": safety_identifier}
)
RequestFields: TypeAlias = SystemOneRequestFields | OpenAIDecisionRequestFields
@dataclass(frozen=True, slots=True, repr=False)
class DecisionsCall:
model: str
requested_model: str
custom_llm_provider: str
provider_config: BaseDecisionsConfig
request: DecisionsRequestFormat
ir_request: DecisionsIRRequest
body: Mapping[str, object] = field(repr=False)
api_base: str
api_key: str | None = field(repr=False)
logging_obj: LiteLLMLoggingObj | None
headers: Mapping[str, str]
timeout: float | httpx.Timeout | None
litellm_params: Mapping[str, object] = field(repr=False)
def supported_decisions_providers() -> tuple[str, ...]:
return tuple(
provider.value
for provider in LlmProviders
if ProviderConfigManager.get_provider_decisions_config(model="", provider=provider) is not None
)
def _provider_config(model: str, custom_llm_provider: str) -> BaseDecisionsConfig:
provider: Final = next((member for member in LlmProviders if member.value == custom_llm_provider), None)
provider_config: Final = (
None if provider is None else ProviderConfigManager.get_provider_decisions_config(model, provider)
)
if provider_config is None:
supported: Final = ", ".join(supported_decisions_providers())
raise litellm.BadRequestError(
message=f"Unknown Decisions provider '{custom_llm_provider}'. Supported providers: {supported}",
model=model,
llm_provider=custom_llm_provider,
)
return provider_config
def _ir_request(request: DecisionsRequestFormat, safety_identifier: str | None) -> DecisionsIRRequest:
match request:
case DecisionsRequestBody():
return replace(systemone_request_to_ir(request), safety_identifier=safety_identifier)
case OpenAIDecisionRequestBody():
return openai_request_to_ir(request)
case _:
assert_never(request)
def _drops_params(kwargs: Mapping[str, object]) -> bool:
return litellm.drop_params is True or normalize_drop_params(kwargs.get("drop_params")) is True
def _request_safety_identifier(safety_identifier: object, kwargs: Mapping[str, object]) -> str | None:
if isinstance(safety_identifier, str) or not _drops_params(kwargs):
return _SAFETY_IDENTIFIER_ADAPTER.validate_python(safety_identifier)
return None
@dataclass(frozen=True, slots=True)
class _UnsupportedSafetyIdentifier:
pass
def _provider_ir_request(
request: DecisionsRequestFormat,
*,
safety_identifier: str | None,
provider_config: BaseDecisionsConfig,
kwargs: Mapping[str, object],
) -> DecisionsIRRequest | _UnsupportedSafetyIdentifier:
ir_request: Final = _ir_request(request, safety_identifier)
if ir_request.safety_identifier is None or provider_config.supports_safety_identifier:
return ir_request
if _drops_params(kwargs):
return replace(ir_request, safety_identifier=None)
return _UnsupportedSafetyIdentifier()
def prepare_call(
*,
model: str,
fields: RequestFields,
safety_identifier: str | None,
api_key: str | None,
api_base: str | None,
timeout: float | httpx.Timeout | None,
custom_llm_provider: str | None,
extra_headers: Mapping[str, str] | None,
kwargs: Mapping[str, object],
) -> DecisionsCall:
upstream_model, provider, dynamic_api_key, dynamic_api_base = litellm.get_llm_provider(
model=model,
custom_llm_provider=custom_llm_provider,
api_base=api_base,
api_key=api_key,
)
provider_config: Final = _provider_config(upstream_model, provider)
if not upstream_model:
raise litellm.BadRequestError(
message="A model name is required for the Decisions API",
model=model,
llm_provider=provider,
)
try:
canonical_model: Final = provider_config.canonical_model(upstream_model)
except ValueError as error:
raise litellm.BadRequestError(message=str(error), model=model, llm_provider=provider) from error
try:
request_safety_identifier: Final = _request_safety_identifier(safety_identifier, kwargs)
request: Final = fields.validate(request_safety_identifier)
except ValidationError as error:
raise litellm.BadRequestError(
message=f"Invalid Decisions request: {error}",
model=model,
llm_provider=provider,
) from error
resolved_api_base: Final = provider_config.resolve_api_base(dynamic_api_base or api_base)
if resolved_api_base is None:
raise litellm.BadRequestError(
message=provider_config.missing_api_base_message(provider),
model=model,
llm_provider=provider,
)
resolved_api_key: Final = provider_config.resolve_api_key(dynamic_api_key or api_key)
if resolved_api_key is None and provider_config.api_key_required:
raise litellm.AuthenticationError(
message=f"Missing API key for Decisions provider '{provider}'",
model=model,
llm_provider=provider,
)
ir_request: Final = _provider_ir_request(
request,
safety_identifier=request_safety_identifier,
provider_config=provider_config,
kwargs=kwargs,
)
if isinstance(ir_request, _UnsupportedSafetyIdentifier):
raise litellm.UnsupportedParamsError(
message=(
f"{provider} does not support parameters: ['safety_identifier'], for model={model}. "
"To drop these, set `litellm.drop_params=True` or for proxy:\n\n"
"`litellm_settings:\n drop_params: true`\n"
),
model=model,
llm_provider=provider,
)
body: Final = provider_config.transform_decisions_request(
model=canonical_model, request=ir_request, custom_llm_provider=provider
)
if isinstance(body, UnsupportedDecisionsRequest):
raise litellm.BadRequestError(
message=f"Decisions provider '{provider}' cannot serve this request: {body.reason}",
model=model,
llm_provider=provider,
)
logging_obj: Final = kwargs.get("litellm_logging_obj")
if isinstance(logging_obj, LiteLLMLoggingObj):
logging_obj.update_from_kwargs(
kwargs=dict(kwargs),
model=canonical_model,
litellm_params={
"litellm_call_id": kwargs.get("litellm_call_id"),
"api_base": provider_config.get_complete_url(resolved_api_base, canonical_model),
},
custom_llm_provider=provider,
)
return DecisionsCall(
model=canonical_model,
requested_model=model,
custom_llm_provider=provider,
provider_config=provider_config,
request=request,
ir_request=ir_request,
body=body,
api_base=resolved_api_base,
api_key=resolved_api_key,
logging_obj=logging_obj if isinstance(logging_obj, LiteLLMLoggingObj) else None,
headers=extra_headers or {},
timeout=timeout,
litellm_params=kwargs,
)
def _format_response(response: DecisionsIRResponse, call: DecisionsCall) -> DecisionsResponse | OpenAIDecisionResponse:
formatted: Final = _formatted_response(response, call)
provider_cost: Final = call.provider_config.provider_reported_cost(response)
formatted.set_hidden_params(
{
"model": f"{call.custom_llm_provider}/{call.model}",
"custom_llm_provider": call.custom_llm_provider,
"provider_response_model": f"{call.custom_llm_provider}/{call.model}",
**({"additional_headers": {RESPONSE_COST_HEADER: provider_cost}} if provider_cost is not None else {}),
}
)
return formatted
def _formatted_response(
response: DecisionsIRResponse, call: DecisionsCall
) -> DecisionsResponse | OpenAIDecisionResponse:
match call.request:
case DecisionsRequestBody():
return ir_to_systemone_response(response, call.ir_request)
case OpenAIDecisionRequestBody():
return ir_to_openai_response(response, call.ir_request, call.requested_model)
case _:
assert_never(call.request)
def _map_upstream_exception(error: Exception, call: DecisionsCall) -> Exception:
if isinstance(error, BaseLLMException) and error.status_code_is_synthesized:
provider_label: Final = f"{call.custom_llm_provider[0].upper()}{call.custom_llm_provider[1:]}Exception"
return litellm.APIConnectionError(
message=f"{provider_label} - {error.message}",
llm_provider=call.custom_llm_provider,
model=f"{call.custom_llm_provider}/{call.model}",
)
return litellm.exception_type(
model=f"{call.custom_llm_provider}/{call.model}",
custom_llm_provider=call.custom_llm_provider,
original_exception=error,
)
async def asend(call: DecisionsCall) -> DecisionsResponse | OpenAIDecisionResponse:
try:
response: Final = await _HANDLER.adecisions(
model=call.model,
custom_llm_provider=call.custom_llm_provider,
logging_obj=call.logging_obj,
provider_config=call.provider_config,
request=call.ir_request,
body=call.body,
api_base=call.api_base,
api_key=call.api_key,
headers=call.headers,
timeout=call.timeout,
litellm_params=call.litellm_params,
)
except Exception as error:
raise _map_upstream_exception(error, call) from error
return _format_response(response, call)
def send(call: DecisionsCall) -> DecisionsResponse | OpenAIDecisionResponse:
try:
response: Final = _HANDLER.decisions(
model=call.model,
custom_llm_provider=call.custom_llm_provider,
logging_obj=call.logging_obj,
provider_config=call.provider_config,
request=call.ir_request,
body=call.body,
api_base=call.api_base,
api_key=call.api_key,
headers=call.headers,
timeout=call.timeout,
litellm_params=call.litellm_params,
)
except Exception as error:
raise _map_upstream_exception(error, call) from error
return _format_response(response, call)
def reject_other_format(*, model: str, kwargs: Mapping[str, object], field_name: str, other_function: str) -> None:
if field_name not in kwargs:
return
raise litellm.BadRequestError(
message=f"Requests with '{field_name}' go through litellm.{other_function}",
model=model,
llm_provider="",
)

View file

@ -1,116 +1,56 @@
from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field
from typing import Final, TypeAlias
import warnings
from collections.abc import Mapping
from typing import Final
import httpx
from pydantic import TypeAdapter, ValidationError
from typing_extensions import assert_never
from pydantic import TypeAdapter
import litellm
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.llms.base_llm.decisions.transformation import (
BaseDecisionsConfig,
ir_to_systemone_response,
systemone_request_to_ir,
from litellm.decisions.call import (
DecisionsCall,
OpenAIDecisionQuestions,
OpenAIDecisionRequestFields,
RequestFields,
SystemOneQuestions,
SystemOneRequestFields,
asend,
prepare_call,
send,
)
from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler
from litellm.llms.openai.decisions.transformation import ir_to_openai_response, openai_request_to_ir
from litellm.types.decisions import (
DecisionQuestion,
DecisionsIRRequest,
DecisionsIRResponse,
DecisionsJSON,
DecisionsRequestBody,
DecisionsResponse,
OpenAIDecisionInput,
OpenAIDecisionQuestion,
OpenAIDecisionRequestBody,
OpenAIDecisionResponse,
UnsupportedDecisionsRequest,
from litellm.types.decisions import DecisionsJSON, DecisionsResponse, OpenAIDecisionInput, OpenAIDecisionResponse
from litellm.utils import client
_STATE_ADAPTER: Final[TypeAdapter[DecisionsJSON | None]] = TypeAdapter(DecisionsJSON | None)
_SYSTEMONE_QUESTIONS_ADAPTER: Final[TypeAdapter[SystemOneQuestions | None]] = TypeAdapter(SystemOneQuestions | None)
_OPENAI_QUESTIONS_ADAPTER: Final[TypeAdapter[OpenAIDecisionQuestions | None]] = TypeAdapter(
OpenAIDecisionQuestions | None
)
from litellm.types.utils import LlmProviders
from litellm.utils import ProviderConfigManager, client
DecisionsQuestions: TypeAlias = (
Mapping[str, DecisionQuestion | Mapping[str, object]] | Sequence[OpenAIDecisionQuestion | Mapping[str, object]]
)
DecisionsRequestFormat: TypeAlias = DecisionsRequestBody | OpenAIDecisionRequestBody
_SYSTEMONE_REQUEST_ADAPTER: Final[TypeAdapter[DecisionsRequestBody]] = TypeAdapter(DecisionsRequestBody)
_OPENAI_REQUEST_ADAPTER: Final[TypeAdapter[OpenAIDecisionRequestBody]] = TypeAdapter(OpenAIDecisionRequestBody)
_HANDLER: Final = BaseLLMHTTPHandler()
@dataclass(frozen=True, slots=True, repr=False)
class _DecisionsCall:
model: str
requested_model: str
custom_llm_provider: str
provider_config: BaseDecisionsConfig
request: DecisionsRequestFormat
ir_request: DecisionsIRRequest
body: Mapping[str, object] = field(repr=False)
api_base: str
api_key: str | None = field(repr=False)
logging_obj: LiteLLMLoggingObj | None
headers: Mapping[str, str]
timeout: float | httpx.Timeout | None
def supported_decisions_providers() -> tuple[str, ...]:
return tuple(
provider.value
for provider in LlmProviders
if ProviderConfigManager.get_provider_decisions_config(model="", provider=provider) is not None
)
def _provider_config(model: str, custom_llm_provider: str) -> BaseDecisionsConfig:
provider: Final = next((member for member in LlmProviders if member.value == custom_llm_provider), None)
provider_config: Final = (
None if provider is None else ProviderConfigManager.get_provider_decisions_config(model, provider)
)
if provider_config is None:
supported: Final = ", ".join(supported_decisions_providers())
raise litellm.BadRequestError(
message=f"Unknown Decisions provider '{custom_llm_provider}'. Supported providers: {supported}",
model=model,
llm_provider=custom_llm_provider,
)
return provider_config
def _validate_request(
*,
state: DecisionsJSON | None,
questions: DecisionsQuestions | None,
def _request_fields(
decision_input: OpenAIDecisionInput | None,
safety_identifier: str | None,
) -> DecisionsRequestFormat:
if decision_input is None:
return _SYSTEMONE_REQUEST_ADAPTER.validate_python({"state": state, "questions": questions})
return _OPENAI_REQUEST_ADAPTER.validate_python(
{"input": decision_input, "questions": questions, "safety_identifier": safety_identifier}
questions: OpenAIDecisionQuestions | SystemOneQuestions | None,
kwargs: Mapping[str, object],
) -> RequestFields:
if "state" not in kwargs:
return OpenAIDecisionRequestFields(
input=decision_input, questions=_OPENAI_QUESTIONS_ADAPTER.validate_python(questions)
)
warnings.warn(
"litellm.decisions(state=...) is deprecated, call litellm.systemone for the System One request format",
UserWarning,
stacklevel=5,
)
return SystemOneRequestFields(
state=_STATE_ADAPTER.validate_python(kwargs["state"]),
questions=_SYSTEMONE_QUESTIONS_ADAPTER.validate_python(questions),
)
def _ir_request(request: DecisionsRequestFormat) -> DecisionsIRRequest:
match request:
case DecisionsRequestBody():
return systemone_request_to_ir(request)
case OpenAIDecisionRequestBody():
return openai_request_to_ir(request)
case _:
assert_never(request)
def _prepare_call(
def _prepare_decisions_call(
*,
model: str,
state: DecisionsJSON | None,
questions: DecisionsQuestions | None,
decision_input: OpenAIDecisionInput | None,
questions: OpenAIDecisionQuestions | SystemOneQuestions | None,
safety_identifier: str | None,
api_key: str | None,
api_base: str | None,
@ -118,149 +58,37 @@ def _prepare_call(
custom_llm_provider: str | None,
extra_headers: Mapping[str, str] | None,
kwargs: Mapping[str, object],
) -> _DecisionsCall:
upstream_model, provider, dynamic_api_key, dynamic_api_base = litellm.get_llm_provider(
) -> DecisionsCall:
return prepare_call(
model=model,
custom_llm_provider=custom_llm_provider,
api_base=api_base,
fields=_request_fields(decision_input, questions, kwargs),
safety_identifier=safety_identifier,
api_key=api_key,
)
provider_config: Final = _provider_config(upstream_model, provider)
canonical_model: Final = provider_config.canonical_model(upstream_model)
if not upstream_model:
raise litellm.BadRequestError(
message="A model name is required for the Decisions API",
model=model,
llm_provider=provider,
)
if state is not None and decision_input is not None:
raise litellm.BadRequestError(
message="Pass either state (System One format) or input (OpenAI format) to the Decisions API, not both",
model=model,
llm_provider=provider,
)
try:
request: Final = _validate_request(
state=state, questions=questions, decision_input=decision_input, safety_identifier=safety_identifier
)
except ValidationError as error:
raise litellm.BadRequestError(
message=f"Invalid Decisions request: {error}",
model=model,
llm_provider=provider,
) from error
resolved_api_base: Final = provider_config.resolve_api_base(dynamic_api_base or api_base)
if resolved_api_base is None:
raise litellm.BadRequestError(
message=provider_config.missing_api_base_message(provider),
model=model,
llm_provider=provider,
)
resolved_api_key: Final = provider_config.resolve_api_key(dynamic_api_key or api_key)
if resolved_api_key is None and provider_config.api_key_required:
raise litellm.AuthenticationError(
message=f"Missing API key for Decisions provider '{provider}'",
model=model,
llm_provider=provider,
)
ir_request: Final = _ir_request(request)
body: Final = provider_config.transform_decisions_request(
model=canonical_model, request=ir_request, custom_llm_provider=provider
)
if isinstance(body, UnsupportedDecisionsRequest):
raise litellm.BadRequestError(
message=f"Decisions provider '{provider}' cannot serve this request: {body.reason}",
model=model,
llm_provider=provider,
)
logging_obj: Final = kwargs.get("litellm_logging_obj")
if isinstance(logging_obj, LiteLLMLoggingObj):
logging_obj.update_from_kwargs(
kwargs=dict(kwargs),
model=canonical_model,
litellm_params={
"litellm_call_id": kwargs.get("litellm_call_id"),
"api_base": provider_config.get_complete_url(resolved_api_base, canonical_model),
},
custom_llm_provider=provider,
)
return _DecisionsCall(
model=canonical_model,
requested_model=model,
custom_llm_provider=provider,
provider_config=provider_config,
request=request,
ir_request=ir_request,
body=body,
api_base=resolved_api_base,
api_key=resolved_api_key,
logging_obj=logging_obj if isinstance(logging_obj, LiteLLMLoggingObj) else None,
headers=extra_headers or {},
api_base=api_base,
timeout=timeout,
)
def _format_response(response: DecisionsIRResponse, call: _DecisionsCall) -> DecisionsResponse | OpenAIDecisionResponse:
formatted: Final = _formatted_response(response, call)
formatted.set_hidden_params(
{
"model": f"{call.custom_llm_provider}/{call.model}",
"custom_llm_provider": call.custom_llm_provider,
"provider_response_model": f"{call.custom_llm_provider}/{call.model}",
}
)
return formatted
def _formatted_response(
response: DecisionsIRResponse, call: _DecisionsCall
) -> DecisionsResponse | OpenAIDecisionResponse:
match call.request:
case DecisionsRequestBody():
return ir_to_systemone_response(response, call.ir_request)
case OpenAIDecisionRequestBody():
return ir_to_openai_response(response, call.ir_request, call.requested_model)
case _:
assert_never(call.request)
def _map_upstream_exception(error: Exception, call: _DecisionsCall) -> Exception:
if isinstance(error, BaseLLMException) and error.status_code_is_synthesized:
provider_label: Final = f"{call.custom_llm_provider[0].upper()}{call.custom_llm_provider[1:]}Exception"
return litellm.APIConnectionError(
message=f"{provider_label} - {error.message}",
llm_provider=call.custom_llm_provider,
model=f"{call.custom_llm_provider}/{call.model}",
)
return litellm.exception_type(
model=f"{call.custom_llm_provider}/{call.model}",
custom_llm_provider=call.custom_llm_provider,
original_exception=error,
custom_llm_provider=custom_llm_provider,
extra_headers=extra_headers,
kwargs={key: value for key, value in kwargs.items() if key != "state"},
)
@client
async def adecisions(
model: str,
state: DecisionsJSON | None = None,
questions: DecisionsQuestions | None = None,
input: OpenAIDecisionInput | None = None,
questions: OpenAIDecisionQuestions | SystemOneQuestions | None = None,
safety_identifier: str | None = None,
api_key: str | None = None,
api_base: str | None = None,
timeout: float | httpx.Timeout | None = None,
custom_llm_provider: str | None = None,
extra_headers: Mapping[str, str] | None = None,
input: OpenAIDecisionInput | None = None,
safety_identifier: str | None = None,
**kwargs: object,
) -> DecisionsResponse | OpenAIDecisionResponse:
call: Final = _prepare_call(
) -> OpenAIDecisionResponse | DecisionsResponse:
call: Final = _prepare_decisions_call(
model=model,
state=state,
questions=questions,
decision_input=input,
questions=questions,
safety_identifier=safety_identifier,
api_key=api_key,
api_base=api_base,
@ -269,43 +97,26 @@ async def adecisions(
extra_headers=extra_headers,
kwargs=kwargs,
)
try:
response: Final = await _HANDLER.adecisions(
model=call.model,
custom_llm_provider=call.custom_llm_provider,
logging_obj=call.logging_obj,
provider_config=call.provider_config,
request=call.ir_request,
body=call.body,
api_base=call.api_base,
api_key=call.api_key,
headers=call.headers,
timeout=call.timeout,
)
except Exception as error:
raise _map_upstream_exception(error, call) from error
return _format_response(response, call)
return await asend(call)
@client
def decisions(
model: str,
state: DecisionsJSON | None = None,
questions: DecisionsQuestions | None = None,
input: OpenAIDecisionInput | None = None,
questions: OpenAIDecisionQuestions | SystemOneQuestions | None = None,
safety_identifier: str | None = None,
api_key: str | None = None,
api_base: str | None = None,
timeout: float | httpx.Timeout | None = None,
custom_llm_provider: str | None = None,
extra_headers: Mapping[str, str] | None = None,
input: OpenAIDecisionInput | None = None,
safety_identifier: str | None = None,
**kwargs: object,
) -> DecisionsResponse | OpenAIDecisionResponse:
call: Final = _prepare_call(
) -> OpenAIDecisionResponse | DecisionsResponse:
call: Final = _prepare_decisions_call(
model=model,
state=state,
questions=questions,
decision_input=input,
questions=questions,
safety_identifier=safety_identifier,
api_key=api_key,
api_base=api_base,
@ -314,22 +125,7 @@ def decisions(
extra_headers=extra_headers,
kwargs=kwargs,
)
try:
response: Final = _HANDLER.decisions(
model=call.model,
custom_llm_provider=call.custom_llm_provider,
logging_obj=call.logging_obj,
provider_config=call.provider_config,
request=call.ir_request,
body=call.body,
api_base=call.api_base,
api_key=call.api_key,
headers=call.headers,
timeout=call.timeout,
)
except Exception as error:
raise _map_upstream_exception(error, call) from error
return _format_response(response, call)
return send(call)
__all__ = ["adecisions", "decisions"]

View file

@ -3,7 +3,7 @@ Helper functions for health check calls.
"""
import base64
from collections.abc import Awaitable, Callable
from collections.abc import Awaitable, Callable, Mapping
from typing import TYPE_CHECKING, Final, Literal
from litellm.llms.base_llm.ocr.transformation import DocumentType
@ -56,6 +56,21 @@ def get_image_file_for_health_check() -> bytes:
return base64.b64decode(TEST_IMAGE_BASE64)
def _decisions_health_check_questions(model: str, custom_llm_provider: str) -> Mapping[str, Mapping[str, object]]:
import litellm
from litellm.llms.base_llm.decisions.transformation import BaseDecisionsConfig
from litellm.utils import ProviderConfigManager
provider: Final = next((member for member in litellm.LlmProviders if member.value == custom_llm_provider), None)
config: Final = (
None
if provider is None
else ProviderConfigManager.get_provider_decisions_config(model=model, provider=provider)
)
questions: Final = BaseDecisionsConfig.health_check_questions if config is None else config.health_check_questions
return {name: dict(question) for name, question in questions.items()}
def _ocr_health_check_document(model: str, custom_llm_provider: str) -> DocumentType:
native: Final = NATIVE_OCR_HEALTH_CHECK_DOCUMENT.load()
if native is None:
@ -296,11 +311,13 @@ class HealthCheckHelpers:
**_filter_model_params(model_params=model_params),
document=_ocr_health_check_document(model=model, custom_llm_provider=custom_llm_provider),
),
"evaluation": lambda: litellm.adecisions(
"evaluation": lambda: litellm.asystemone(
**DECISIONS_CALL_PARAMS.validate_python(
{
"state": prompt or "health check",
"questions": {"reachable": {"type": "noul", "instructions": "Is the service reachable?"}},
"questions": _decisions_health_check_questions(
model=model, custom_llm_provider=custom_llm_provider
),
**_filter_model_params(model_params=model_params),
}
)

View file

@ -301,10 +301,17 @@ class BaseDecisionsConfig(ABC):
api_key_env: tuple[str, ...] = ()
api_base_env: tuple[str, ...] = ()
api_key_required: bool = True
supports_safety_identifier: bool = False
health_check_questions: Mapping[str, Mapping[str, object]] = MappingProxyType(
{"reachable": MappingProxyType({"type": "noul", "instructions": "Is the service reachable?"})}
)
def get_default_api_base(self) -> str | None:
return None
def provider_reported_cost(self, response: DecisionsIRResponse) -> float | None:
return None
def missing_api_base_message(self, custom_llm_provider: str) -> str:
return f"api_base is required for Decisions provider '{custom_llm_provider}'"
@ -334,6 +341,20 @@ class BaseDecisionsConfig(ABC):
def get_complete_url(self, api_base: str, model: str) -> str:
return f"{api_base.rstrip('/').removesuffix('/v1')}{self.path}"
def signs_with_aws(self, api_base: str) -> bool:
return False
def sign_request(
self,
headers: Mapping[str, str],
url: str,
api_base: str,
body: Mapping[str, object],
api_key: str | None,
litellm_params: Mapping[str, object],
) -> tuple[Mapping[str, str], bytes | None]:
return headers, None
def transform_decisions_request(
self,
model: str,

View file

@ -4,7 +4,7 @@ import json
import ssl
from collections.abc import AsyncGenerator, AsyncIterator, Awaitable, Coroutine, Iterator, Mapping, Sequence
from contextlib import asynccontextmanager
from functools import lru_cache
from functools import lru_cache, partial
from types import MappingProxyType
from typing import (
TYPE_CHECKING,
@ -1517,6 +1517,7 @@ class BaseLLMHTTPHandler:
api_key: str | None,
headers: Mapping[str, str],
timeout: float | httpx.Timeout | None,
litellm_params: Mapping[str, object],
client: HTTPHandler | None = None,
) -> DecisionsIRResponse:
url, outbound_headers, data = self._prepare_decisions_request(
@ -1528,10 +1529,23 @@ class BaseLLMHTTPHandler:
api_key=api_key,
headers=headers,
)
signed_headers, signed_body = provider_config.sign_request(
headers=outbound_headers,
url=url,
api_base=api_base,
body=data,
api_key=api_key,
litellm_params=litellm_params,
)
sync_httpx_client: Final = client if client is not None else get_httpx_client()
try:
response: Final = sync_httpx_client.post(
url, json=data, headers=outbound_headers, timeout=timeout, logging_obj=logging_obj
url,
content=signed_body,
json=data if signed_body is None else None,
headers=dict(signed_headers),
timeout=timeout,
logging_obj=logging_obj,
)
except httpx.HTTPError as e:
raise self._handle_error(e=e, provider_config=provider_config)
@ -1551,6 +1565,7 @@ class BaseLLMHTTPHandler:
api_key: str | None,
headers: Mapping[str, str],
timeout: float | httpx.Timeout | None,
litellm_params: Mapping[str, object],
client: AsyncHTTPHandler | None = None,
) -> DecisionsIRResponse:
url, outbound_headers, data = self._prepare_decisions_request(
@ -1562,6 +1577,18 @@ class BaseLLMHTTPHandler:
api_key=api_key,
headers=headers,
)
sign: Final = partial(
provider_config.sign_request,
headers=outbound_headers,
url=url,
api_base=api_base,
body=data,
api_key=api_key,
litellm_params=litellm_params,
)
signed_headers, signed_body = (
await run_aws_signing(sign) if provider_config.signs_with_aws(api_base) else sign()
)
async_httpx_client: Final = (
client
if client is not None
@ -1569,7 +1596,12 @@ class BaseLLMHTTPHandler:
)
try:
response: Final = await async_httpx_client.post(
url, json=data, headers=outbound_headers, timeout=timeout, logging_obj=logging_obj
url,
content=signed_body,
json=data if signed_body is None else None,
headers=dict(signed_headers),
timeout=timeout,
logging_obj=logging_obj,
)
except httpx.HTTPError as e:
raise self._handle_error(e=e, provider_config=provider_config)

View file

@ -0,0 +1,57 @@
import re
from collections.abc import Mapping
from dataclasses import dataclass, field
from types import MappingProxyType
from typing import Final
from litellm.llms.base_llm.decisions.transformation import BaseDecisionsConfig
_SERVING_ENDPOINT_NAME: Final = re.compile(r"[A-Za-z0-9_-][A-Za-z0-9._-]*")
def validate_serving_endpoint_name(name: str) -> str:
if _SERVING_ENDPOINT_NAME.fullmatch(name) is None:
raise ValueError(
f"Databricks serving endpoint name {name!r} must be the bare endpoint name (letters, digits, '-', '_' "
"and '.', with no '/', '?', '#', spaces, or a leading '.'), e.g. databricks-openjev-qwen35-4b"
)
return name
@dataclass(frozen=True, slots=True)
class DatabricksDecisionsConnection:
api_base: str
api_key: str = field(repr=False)
class DatabricksDecisionsConfig(BaseDecisionsConfig):
api_key_env = ("DATABRICKS_API_KEY", "DATABRICKS_TOKEN")
api_base_env = ("DATABRICKS_API_BASE",)
def missing_api_base_message(self, custom_llm_provider: str) -> str:
return (
f"api_base is required for Decisions provider '{custom_llm_provider}': set DATABRICKS_API_BASE to "
"https://<workspace-host>/serving-endpoints"
)
def canonical_model(self, model: str) -> str:
return validate_serving_endpoint_name(model)
def get_complete_url(self, api_base: str, model: str) -> str:
return f"{api_base.rstrip('/')}/{validate_serving_endpoint_name(model)}/invocations"
def classifier_response(self, body: Mapping[str, object], requested_model: str) -> Mapping[str, object]:
return MappingProxyType({**body, "model": requested_model})
def connection(self, api_base: str | None, api_key: str | None) -> DatabricksDecisionsConnection:
base: Final = self.resolve_api_base(api_base)
key: Final = api_key if api_base is not None else self.resolve_api_key(api_key)
if not base or not key:
raise ValueError(
"Databricks requires api_key or DATABRICKS_API_KEY (or DATABRICKS_TOKEN) and api_base or "
"DATABRICKS_API_BASE pointing to https://<workspace-host>/serving-endpoints"
)
return DatabricksDecisionsConnection(api_base=base.rstrip("/"), api_key=key)
DATABRICKS_DECISIONS_CONFIG: Final[DatabricksDecisionsConfig] = DatabricksDecisionsConfig()

View file

@ -0,0 +1,26 @@
from types import MappingProxyType
from typing import Final
from litellm.constants import PLACEHOLDER_API_KEY
from litellm.llms.base_llm.decisions.transformation import BaseDecisionsConfig
class HostedVLLMDecisionsConfig(BaseDecisionsConfig):
api_key_env = ("HOSTED_VLLM_API_KEY",)
api_base_env = ("HOSTED_VLLM_API_BASE",)
api_key_required = False
health_check_questions = MappingProxyType(
{
"reachable": MappingProxyType(
{
"type": "choice",
"instructions": "Is the service reachable?",
"criteria": {"yes": None, "no": None},
}
)
}
)
def resolve_api_key(self, api_key: str | None) -> str | None:
resolved: Final = super().resolve_api_key(api_key)
return None if resolved == PLACEHOLDER_API_KEY else resolved

View file

@ -292,6 +292,7 @@ def ir_to_openai_response(
class OpenAIDecisionsConfig(BaseDecisionsConfig):
path = "/v1/decisions"
supports_safety_identifier = True
def get_default_api_base(self) -> str | None:
return "https://api.openai.com"

View file

@ -12,7 +12,6 @@ from typing import TYPE_CHECKING, Any, Final, cast
import httpx
import litellm
from litellm.litellm_core_utils.hidden_params import HIDDEN_PARAMS_ATTR, set_hidden_params
from litellm.llms.base_llm.base_model_iterator import BaseModelResponseIterator
from litellm.llms.base_llm.chat.transformation import BaseLLMException
@ -41,18 +40,10 @@ class CacheControlSupportedModels(str, Enum):
class OpenrouterConfig(OpenAIGPTConfig):
def get_supported_openai_params(self, model: str) -> list:
"""
Allow reasoning parameters for models flagged as reasoning-capable.
Include reasoning parameters regardless of model-map capabilities.
"""
supported_params: Final = super().get_supported_openai_params(model=model)
try:
if litellm.supports_reasoning(model=model, custom_llm_provider="openrouter") or litellm.supports_reasoning(
model=model
):
supported_params.append("reasoning_effort")
supported_params.append("thinking")
except Exception:
pass
return list(dict.fromkeys(supported_params))
return list(dict.fromkeys([*supported_params, "reasoning_effort", "thinking"]))
def map_openai_params(
self,

View file

@ -1,4 +1,7 @@
from typing import Final
from litellm.llms.base_llm.decisions.transformation import BaseDecisionsConfig
from litellm.types.decisions import DecisionsIRResponse
class OpenRouterDecisionsConfig(BaseDecisionsConfig):
@ -8,3 +11,9 @@ class OpenRouterDecisionsConfig(BaseDecisionsConfig):
def get_default_api_base(self) -> str | None:
return "https://openrouter.ai/api"
def provider_reported_cost(self, response: DecisionsIRResponse) -> float | None:
cost: Final = response.usage.extra.get("cost")
if not isinstance(cost, (int, float)):
return None
return float(cost)

View file

@ -8,11 +8,25 @@ encrypted_content for multi-turn stateless workflows.
Docs: https://openrouter.ai/docs/api/reference/responses/overview
"""
from collections.abc import Mapping
from typing import Final
import httpx
from pydantic import TypeAdapter
import litellm
from litellm.litellm_core_utils.core_helpers import RESPONSE_COST_HEADER
from litellm.litellm_core_utils.hidden_params import set_hidden_params
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig
from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.openai import (
ResponseCompletedEvent,
ResponseFailedEvent,
ResponseIncompleteEvent,
ResponsesAPIResponse,
ResponsesAPIStreamingResponse,
)
from litellm.types.router import GenericLiteLLMParams
from litellm.types.utils import LlmProviders
@ -33,6 +47,62 @@ class OpenRouterResponsesAPIConfig(OpenAIResponsesAPIConfig):
def custom_llm_provider(self) -> LlmProviders:
return LlmProviders.OPENROUTER
def transform_response_api_response(
self,
model: str,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> ResponsesAPIResponse:
response: Final = super().transform_response_api_response(
model=model,
raw_response=raw_response,
logging_obj=logging_obj,
)
self._set_provider_reported_cost(response)
return response
def transform_streaming_response(
self,
model: str,
parsed_chunk: Mapping[str, object],
logging_obj: LiteLLMLoggingObj,
) -> ResponsesAPIStreamingResponse:
response_event: Final = super().transform_streaming_response(
model=model,
parsed_chunk=dict(parsed_chunk),
logging_obj=logging_obj,
)
terminal_response: Final = (
response_event.response
if isinstance(
response_event,
(ResponseCompletedEvent, ResponseFailedEvent, ResponseIncompleteEvent),
)
else None
)
if terminal_response is not None:
self._set_provider_reported_cost(terminal_response)
return response_event
@staticmethod
def _set_provider_reported_cost(response: ResponsesAPIResponse) -> None:
cost: Final = response.usage.cost if response.usage is not None else None
if cost is None:
return
additional_headers: Final = TypeAdapter(dict[str, object]).validate_python(
response.hidden_params.get("additional_headers", {})
)
set_hidden_params(
response,
{
**response.hidden_params,
"additional_headers": {
**additional_headers,
RESPONSE_COST_HEADER: float(cost),
},
},
)
def validate_environment(
self,
headers: dict,

View file

@ -1,7 +1,161 @@
import hashlib
import json
import re
from collections.abc import Mapping
from dataclasses import dataclass
from functools import partial
from types import MappingProxyType
from typing import Final
from urllib.parse import quote, unquote
import httpx
from pydantic import BaseModel, ConfigDict, ValidationError
from litellm._logging import verbose_logger
from litellm.litellm_core_utils.aws_partition import get_aws_dns_suffix
from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.llms.base_llm.decisions.transformation import BaseDecisionsConfig
from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM, sign_aws_json_post
from litellm.types.decisions import DecisionsIRRequest, DecisionsIRResponse
from litellm.types.llms.bedrock import AwsAuthParams
AGENTCORE_SESSION_HEADER: Final = "X-Amzn-Bedrock-AgentCore-Runtime-Session-Id"
_RESERVED_HEADER_PREFIXES: Final = ("x-amzn-bedrock-agentcore-runtime-", "x-amz-")
_RUNTIME_ARN: Final = re.compile(
r"\Aarn:aws(?:-[a-z]+)*:bedrock-agentcore:(?P<region>[a-z0-9-]+):[0-9]{12}:runtime/[A-Za-z0-9_-]+\Z"
)
_INVOCATIONS_PATH: Final = re.compile(r"\A/runtimes/(?P<arn>[^/]+)/invocations\Z")
_RUNTIME_ERROR_STATUS: Final = MappingProxyType({"bad_request": 400, "invalid_request": 400, "loading": 503})
@dataclass(frozen=True, slots=True)
class AgentCoreRuntime:
arn: str
region: str
@property
def invocations_url(self) -> str:
host: Final = f"bedrock-agentcore.{self.region}.{get_aws_dns_suffix(self.region)}"
return f"https://{host}/runtimes/{quote(self.arn, safe='')}/invocations"
@property
def default_session_id(self) -> str:
return f"litellm-decider-{hashlib.sha256(self.arn.encode()).hexdigest()}"
def agentcore_runtime(api_base: str) -> AgentCoreRuntime | None:
match: Final = _RUNTIME_ARN.match(api_base)
return None if match is None else AgentCoreRuntime(arn=api_base, region=match["region"])
def invoked_runtime(url: httpx.URL) -> AgentCoreRuntime | None:
match: Final = _INVOCATIONS_PATH.match(url.raw_path.decode("ascii"))
runtime: Final = None if match is None else agentcore_runtime(unquote(match["arn"]))
return runtime if runtime is not None and runtime.invocations_url == str(url) else None
class _RuntimeError(BaseModel):
model_config = ConfigDict(frozen=True)
code: str
message: str = ""
class _RuntimeReply(BaseModel):
model_config = ConfigDict(frozen=True)
error: _RuntimeError | None = None
answers: object = None
def _runtime_error(content: bytes) -> _RuntimeError | None:
try:
reply: Final = _RuntimeReply.model_validate_json(content)
except ValidationError:
return None
return reply.error if reply.answers is None else None
class _SessionParams(BaseModel):
model_config = ConfigDict(frozen=True)
agentcore_runtime_session_id: str | None = None
def _is_reserved_header(name: str) -> bool:
lowered: Final = name.lower()
return lowered == "host" or lowered.startswith(_RESERVED_HEADER_PREFIXES)
def _runtime_headers(
headers: Mapping[str, str], runtime: AgentCoreRuntime, litellm_params: Mapping[str, object]
) -> Mapping[str, str]:
dropped: Final = sorted(name for name in headers if _is_reserved_header(name))
if dropped:
verbose_logger.warning("Strands Decider: not forwarding reserved AgentCore header(s) %s", dropped)
configured: Final = _SessionParams.model_validate(litellm_params).agentcore_runtime_session_id
return {
**{name: value for name, value in headers.items() if not _is_reserved_header(name)},
AGENTCORE_SESSION_HEADER: runtime.default_session_id if configured is None else configured,
}
class StrandsDeciderDecisionsConfig(BaseDecisionsConfig):
api_key_env = ("STRANDS_DECIDER_API_KEY",)
api_base_env = ("STRANDS_DECIDER_API_BASE",)
api_key_required = False
def __init__(self, aws: BaseAWSLLM | None = None) -> None:
self._aws: Final = aws if aws is not None else BaseAWSLLM()
def get_complete_url(self, api_base: str, model: str) -> str:
runtime: Final = agentcore_runtime(api_base)
return super().get_complete_url(api_base, model) if runtime is None else runtime.invocations_url
def signs_with_aws(self, api_base: str) -> bool:
return agentcore_runtime(api_base) is not None
def sign_request(
self,
headers: Mapping[str, str],
url: str,
api_base: str,
body: Mapping[str, object],
api_key: str | None,
litellm_params: Mapping[str, object],
) -> tuple[Mapping[str, str], bytes | None]:
runtime: Final = agentcore_runtime(api_base)
if runtime is None:
return headers, None
session_headers: Final = _runtime_headers(headers, runtime, litellm_params)
if api_key is not None:
return session_headers, None
payload: Final = json.dumps(body)
signed: Final = sign_aws_json_post(
get_credentials=partial(
self._aws.resolve_credentials, AwsAuthParams.model_validate(litellm_params), runtime.region
),
service_name="bedrock-agentcore",
aws_region_name=runtime.region,
url=url,
body=payload,
headers=session_headers,
)
return dict(signed.headers.items()), payload.encode()
def transform_decisions_response(
self,
model: str,
custom_llm_provider: str,
raw_response: httpx.Response,
request: DecisionsIRRequest,
) -> DecisionsIRResponse:
runtime_error: Final = (
None if invoked_runtime(raw_response.request.url) is None else _runtime_error(raw_response.content)
)
if runtime_error is not None:
raise BaseLLMException(
status_code=_RUNTIME_ERROR_STATUS.get(runtime_error.code, 500),
message=f"Strands Decider runtime error '{runtime_error.code}': {runtime_error.message}",
)
return super().transform_decisions_response(model, custom_llm_provider, raw_response, request)

View file

@ -21165,6 +21165,18 @@
"output_vector_size": 1024,
"source": "https://www.databricks.com/product/pricing/foundation-model-serving"
},
"databricks/databricks-openjev-qwen35-4b": {
"cache_creation_input_token_cost": 0.0,
"cache_read_input_token_cost": 0.0,
"input_cost_per_token": 0.0,
"litellm_provider": "databricks",
"metadata": {
"notes": "Pay-per-token Foundation Model API decision endpoint (OpenJev, Qwen 3.5 4B) queried through the System One API. Databricks published no DBU rate for it as of 2026-10-07, so a deployment's model_info prices set the billed cost."
},
"mode": "evaluation",
"output_cost_per_token": 0.0,
"source": "https://docs.databricks.com/aws/en/machine-learning/foundation-model-apis/supported-models"
},
"dataforseo/search": {
"input_cost_per_query": 0.003,
"litellm_provider": "dataforseo",

File diff suppressed because it is too large Load diff

View file

@ -411,6 +411,8 @@ _BANNED_REQUEST_BODY_PARAMS: Final[tuple[str, ...]] = (
"anthropic_workspace_id",
"anthropic-workspace-id",
"bedrock_tags",
# Deployment-pinned AgentCore session: every new caller-chosen value provisions another microVM.
"agentcore_runtime_session_id",
# Provider-specific endpoint overrides that flow into the outbound
# request via ``optional_params``. Same threat as ``api_base``:
# ``s3_endpoint_url`` redirects Bedrock file uploads to attacker

View file

@ -182,6 +182,7 @@ ProxyRouteType: TypeAlias = Literal[
"aocr",
"asearch",
"adecisions",
"asystemone",
"avideo_generation",
"avideo_list",
"avideo_status",
@ -1986,6 +1987,7 @@ class ProxyBaseLLMRequestProcessing:
"aocr",
"asearch",
"adecisions",
"asystemone",
"avideo_generation",
"avideo_list",
"avideo_status",

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

@ -7,18 +7,24 @@ immediately can land on a sibling that has never heard of it and fail 400/404.
On a registry miss, callers here fetch the missing row from the DB and load it
into the local registry before giving up. A short negative-result TTL per key
plus a global resync budget per window bound the DB load from lookups of
genuinely unknown names.
genuinely unknown names. An auto router's tiers are deployment rows of their
own, so a miss on an auto router reconciles every row instead of just its own,
or the first routed request on this replica would pick a tier it has not loaded.
"""
import asyncio
import time
from collections.abc import Awaitable, Callable
from collections.abc import Awaitable, Callable, Mapping, Sequence
from typing import TYPE_CHECKING, Final
from pydantic import TypeAdapter, ValidationError
from litellm._logging import verbose_proxy_logger
from litellm.caching.in_memory_cache import InMemoryCache
from litellm.proxy.common_utils.encrypt_decrypt_utils import decrypt_value_helper
if TYPE_CHECKING:
from prisma import models as prisma_models
from prisma.types import (
LiteLLM_AgentsTableInclude,
LiteLLM_AgentsTableWhereUniqueInput,
@ -129,6 +135,11 @@ async def _resync_model_deployments(model_name: str) -> bool:
prisma_client=prisma_client, proxy_logging_obj=proxy_server.proxy_logging_obj
)
return proxy_server.llm_router is not None
if _holds_an_auto_router(rows):
await proxy_server.proxy_config.add_deployment(
prisma_client=prisma_client, proxy_logging_obj=proxy_server.proxy_logging_obj
)
return _model_is_loaded(model_name)
async with proxy_server.MODEL_RECONCILE_LOCK:
await proxy_server.proxy_config.get_credentials(prisma_client=prisma_client)
proxy_server.proxy_config._add_deployment(db_models=rows)
@ -136,6 +147,25 @@ async def _resync_model_deployments(model_name: str) -> bool:
return True
def _holds_an_auto_router(rows: Sequence["prisma_models.LiteLLM_ProxyModelTable"]) -> bool:
return any(_names_an_auto_router(row.litellm_params) for row in rows)
_STORED_LITELLM_PARAMS: Final = TypeAdapter(Mapping[str, object])
def _names_an_auto_router(stored_litellm_params: object) -> bool:
try:
params: Final = _STORED_LITELLM_PARAMS.validate_python(stored_litellm_params)
except ValidationError:
return False
stored_model: Final = params.get("model")
if not isinstance(stored_model, str):
return False
model: Final = decrypt_value_helper(value=stored_model, key="model", return_original_value=True)
return isinstance(model, str) and model.startswith("auto_router/")
async def _resync_guardrails(guardrail_name: str) -> bool:
from litellm.proxy import proxy_server
from litellm.proxy.guardrails.guardrail_registry import (

View file

@ -1,5 +1,6 @@
from collections.abc import Mapping
from typing import Annotated, Final
from types import MappingProxyType
from typing import Annotated, Final, Literal
from fastapi import APIRouter, Depends, Request, Response
from fastapi.responses import ORJSONResponse # pyright: ignore[reportDeprecated] # required endpoint contract
@ -8,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])
@ -19,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(
@ -38,6 +92,10 @@ async def _invalid_request(
)
def _fields_checked_before_routing(data: Mapping[str, object]) -> Mapping[str, object]:
return MappingProxyType({key: value for key, value in data.items() if key != "safety_identifier"})
async def _request_data(request: Request, user_api_key_dict: UserAPIKeyAuth) -> dict[str, object]:
body: Final = await request.body()
try:
@ -51,6 +109,7 @@ async def _process_decisions(
fastapi_response: Response,
user_api_key_dict: UserAPIKeyAuth,
body_adapter: TypeAdapter[DecisionsRequestBody] | TypeAdapter[OpenAIDecisionRequestBody],
route_type: Literal["adecisions", "asystemone"],
) -> object:
from litellm.proxy.proxy_server import (
general_settings as proxy_general_settings,
@ -75,7 +134,7 @@ async def _process_decisions(
data: Final = await _request_data(request, user_api_key_dict)
try:
body_adapter.validate_python(data)
body_adapter.validate_python(_fields_checked_before_routing(data))
except ValidationError as error:
raise await _invalid_request(raw_data=data, error=error, user_api_key_dict=user_api_key_dict)
general_settings: Final = _GENERAL_SETTINGS_ADAPTER.validate_python(proxy_general_settings)
@ -88,7 +147,7 @@ async def _process_decisions(
request=request,
fastapi_response=fastapi_response,
user_api_key_dict=user_api_key_dict,
route_type="adecisions",
route_type=route_type,
proxy_logging_obj=proxy_logging_obj,
llm_router=llm_router,
general_settings=general_settings,
@ -116,23 +175,41 @@ 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,
user_api_key_dict=user_api_key_dict,
body_adapter=_DECISIONS_REQUEST_BODY_ADAPTER,
route_type="asystemone",
)
@ -141,21 +218,39 @@ 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,
user_api_key_dict=user_api_key_dict,
body_adapter=_OPENAI_DECISION_REQUEST_BODY_ADAPTER,
route_type="adecisions",
)

View file

@ -1496,7 +1496,7 @@ async def get_guardrail_ui_settings() -> GuardrailUIAddGuardrailSettings:
- PII entity categories for UI organization
- Content filter settings (patterns and categories)
"""
from litellm.decisions.main import supported_decisions_providers
from litellm.decisions.call import supported_decisions_providers
from litellm.proxy.guardrails.guardrail_hooks.litellm_content_filter.patterns import (
PATTERN_CATEGORIES,
get_available_content_categories,

View file

@ -76,7 +76,7 @@ class _MemberOpenSourceClassifierConfig(LiteLLMBaseModel):
model_config = ConfigDict(extra="forbid")
provider: Literal["jev", "laya", "bespoke"] = "jev"
provider: Literal["jev", "laya", "bespoke", "databricks"] = "jev"
model: str
api_key: None = None
api_base: None = None

View file

@ -21,6 +21,7 @@ if TYPE_CHECKING:
class _TypeSafeUsage(LiteLLMBaseModel):
input_tokens: int = 0
output_tokens: int = 0
cost: float | None = None
class _TypeSafeResponse(LiteLLMBaseModel):
@ -88,7 +89,9 @@ class TypeSafePassthroughLoggingHandler:
)
pricing: Final = _pricing_for(candidate_model_keys)
response_cost: Final = (
input_tokens * pricing.input_cost_per_token + output_tokens * pricing.output_cost_per_token
usage.cost
if custom_llm_provider == "openrouter" and usage.cost is not None
else input_tokens * pricing.input_cost_per_token + output_tokens * pricing.output_cost_per_token
)
usage_object: Final = Usage(
prompt_tokens=input_tokens,

View file

@ -94,6 +94,7 @@ ROUTE_ENDPOINT_MAPPING: Final = {
"aocr": "/ocr",
"asearch": "/search",
"adecisions": "/decisions",
"asystemone": "/systemone",
"avideo_generation": "/videos",
"avideo_list": "/videos",
"avideo_status": "/videos/{video_id}",
@ -519,6 +520,7 @@ RouteType = Literal[
"aocr",
"asearch",
"adecisions",
"asystemone",
"avideo_generation",
"avideo_list",
"avideo_status",

View file

@ -2232,6 +2232,11 @@ class Router:
self.adecisions = self.factory_function(adecisions, call_type="adecisions")
self.decisions = self.factory_function(decisions, call_type="decisions")
from litellm.systemone import asystemone, systemone
self.asystemone = self.factory_function(asystemone, call_type="asystemone") # pyright: ignore[reportUnknownMemberType] # factory_function takes a bare Callable
self.systemone = self.factory_function(systemone, call_type="systemone") # pyright: ignore[reportUnknownMemberType] # factory_function takes a bare Callable
def _initialize_video_endpoints(self):
"""Initialize video endpoints."""
from litellm.videos import (
@ -6972,6 +6977,8 @@ class Router:
"search",
"adecisions",
"decisions",
"asystemone",
"systemone",
"aadapter_generate_content",
"avideo_generation",
"video_generation",
@ -7046,6 +7053,7 @@ class Router:
"ocr",
"search",
"decisions",
"systemone",
"video_generation",
"video_list",
"video_status",
@ -7214,6 +7222,7 @@ class Router:
"aocr",
"ocr",
"adecisions",
"asystemone",
"avideo_generation",
"avideo_list",
"avideo_status",

View file

@ -59,6 +59,7 @@ from litellm.litellm_core_utils.sensitive_data_masker import mask_credentials_in
from litellm.llms.anthropic.common_utils import is_claude_code_user_agent
from litellm.llms.base_llm.base_utils import type_to_response_format_param
from litellm.llms.custom_httpx.http_handler import get_async_httpx_client
from litellm.llms.databricks.decisions.transformation import DATABRICKS_DECISIONS_CONFIG
from litellm.router_strategy.adaptive_router.classifier import classify_prompt
from litellm.router_strategy.complexity_router.context_compaction import compaction_pending
from litellm.router_strategy.complexity_router.tier_predictor import (
@ -1342,6 +1343,14 @@ class ComplexityRouter(CustomLogger):
http_client=get_async_httpx_client(httpxSpecialProvider.PassThroughEndpoint),
provider=config.provider,
)
if config.provider == "databricks":
databricks: Final = DATABRICKS_DECISIONS_CONFIG.connection(config.api_base, config.api_key)
return HttpJevClassifierClient(
api_key=databricks.api_key,
api_base=databricks.api_base,
http_client=get_async_httpx_client(httpxSpecialProvider.PassThroughEndpoint),
provider="databricks",
)
api_key: Final = config.api_key or get_secret_str("TYPESAFE_API_KEY")
if not api_key:
raise ValueError(

View file

@ -705,10 +705,18 @@ def normalize_classifier_config_aliases(config: Mapping[str, object]) -> Mapping
return normalized
_ENVIRONMENT_KEY_SCOPE: Final = MappingProxyType(
{
"jev": "TYPESAFE_API_KEY is only sent to TYPESAFE_API_BASE or https://api.typesafe.ai",
"databricks": "DATABRICKS_API_KEY or DATABRICKS_TOKEN is only sent to DATABRICKS_API_BASE",
}
)
class OpenSourceClassifierConfig(LiteLLMBaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
provider: Literal["jev", "laya", "bespoke"] = "jev"
provider: Literal["jev", "laya", "bespoke", "databricks"] = "jev"
model: str = "jev-latest"
api_key: str | None = Field(default=None, description="Provider API key; optional for self-hosted providers")
api_base: str | None = Field(
@ -753,10 +761,19 @@ class OpenSourceClassifierConfig(LiteLLMBaseModel):
if self.api_base is not None:
_ = validate_oss_api_base(self.provider, self.api_base)
return self
if self.provider == "databricks":
from litellm.llms.databricks.decisions.transformation import validate_serving_endpoint_name
if "model" not in self.model_fields_set:
raise ValueError(
"opensource_classifier_config.model is required for provider 'databricks': the serving endpoint "
"name, e.g. databricks-openjev-qwen35-4b"
)
_ = validate_serving_endpoint_name(self.model)
if self.api_base is not None and self.api_key is None:
raise ValueError(
"opensource_classifier_config.api_base requires opensource_classifier_config.api_key: TYPESAFE_API_KEY is only sent "
"to TYPESAFE_API_BASE or https://api.typesafe.ai"
"opensource_classifier_config.api_base requires opensource_classifier_config.api_key: "
f"{_ENVIRONMENT_KEY_SCOPE[self.provider]}"
)
return self

View file

@ -18,6 +18,7 @@ from litellm.litellm_core_utils.internal_call_metadata import (
from litellm.litellm_core_utils.litellm_logging import Logging
from litellm.litellm_core_utils.logging_worker import GLOBAL_LOGGING_WORKER
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler
from litellm.llms.databricks.decisions.transformation import DATABRICKS_DECISIONS_CONFIG
from litellm.llms.laya.common_utils import laya_response_model
from litellm.proxy.pass_through_endpoints.llm_provider_handlers.typesafe_passthrough_logging_handler import (
TypeSafePassthroughLoggingHandler,
@ -27,6 +28,7 @@ from litellm.types.llms.base import LiteLLMBaseModel
from litellm.types.utils import AUTOROUTER_CLASSIFIER_CALL_ORIGIN
JevProbability: TypeAlias = Annotated[float, Field(ge=0.0, le=1.0)]
ClassifierProvider: TypeAlias = Literal["typesafe", "laya", "bespoke", "databricks"]
DEFAULT_JEV_INSTRUCTIONS: Final = _DEFAULT_JEV_INSTRUCTIONS
@ -85,12 +87,12 @@ class HttpJevClassifierClient:
api_key: str | None,
api_base: str,
http_client: AsyncHTTPHandler,
provider: Literal["typesafe", "laya", "bespoke"] = "typesafe",
provider: ClassifierProvider = "typesafe",
) -> None:
self._api_key = api_key
self._api_base = api_base.rstrip("/")
self._http_client = http_client
self._provider = provider
self._provider: ClassifierProvider = provider
async def evaluate(
self,
@ -103,33 +105,43 @@ class HttpJevClassifierClient:
MappingProxyType({"Authorization": f"Bearer {self._api_key}"}) if self._api_key else MappingProxyType({})
)
response: Final = await self._http_client.post( # pyright: ignore[reportUnknownMemberType] # AsyncHTTPHandler has a dynamic post signature
f"{self._api_base}/v1/systemone",
self._request_url(request.model),
json=request.model_dump(mode="json"),
headers=MappingProxyType({**authorization, "Content-Type": "application/json"}), # pyright: ignore[reportArgumentType] # HTTP headers are not mutated by AsyncHTTPHandler
timeout=timeout_s,
)
response.raise_for_status()
body: Final = TypeAdapter(dict[str, object]).validate_json(response.content)
normalized_body: Final = (
MappingProxyType({**body, "model": laya_response_model(body, request.model)})
if self._provider == "laya"
else body
)
normalized_body: Final = self._normalized_body(body, request.model)
try:
self._log_response(request, response, request_kwargs, start_time)
self._log_response(request, response, normalized_body, request_kwargs, start_time)
except Exception as exc: # noqa: BLE001 # logging integrations must not discard a provider verdict
verbose_router_logger.warning("JEV response logging failed (%s)", type(exc).__name__)
return TypeAdapter(JevSystemOneResponse).validate_python(normalized_body)
def _request_url(self, model: str) -> str:
if self._provider == "databricks":
return DATABRICKS_DECISIONS_CONFIG.get_complete_url(self._api_base, model)
return f"{self._api_base}/v1/systemone"
def _normalized_body(self, body: Mapping[str, object], requested_model: str) -> Mapping[str, object]:
match self._provider:
case "laya":
return MappingProxyType({**body, "model": laya_response_model(body, requested_model)})
case "databricks":
return DATABRICKS_DECISIONS_CONFIG.classifier_response(body, requested_model)
case "typesafe" | "bespoke":
return body
def _log_response(
self,
request: JevSystemOneRequest,
response: httpx.Response,
body: Mapping[str, object],
request_kwargs: Mapping[str, object] | None,
start_time: datetime,
) -> None:
try:
body: Final = TypeAdapter(dict[str, object]).validate_json(response.content)
_ = TypeAdapter(JevUsage | None).validate_python(body.get("usage"))
except ValidationError:
return
@ -202,7 +214,7 @@ class JevVerdict(NamedTuple):
confidence: float
model: str
cost: float | None
provider: Literal["typesafe", "laya", "bespoke"] = "typesafe"
provider: ClassifierProvider = "typesafe"
class _RegistryPricing(LiteLLMBaseModel):
@ -226,7 +238,7 @@ def build_jev_request(
def jev_classifier_cost(
response: JevSystemOneResponse, configured_model: str, provider: Literal["typesafe", "laya", "bespoke"] = "typesafe"
response: JevSystemOneResponse, configured_model: str, provider: ClassifierProvider = "typesafe"
) -> float | None:
usage: Final = response.usage
if usage is None:

View file

@ -0,0 +1,3 @@
from litellm.systemone.main import asystemone, systemone
__all__ = ["asystemone", "systemone"]

107
litellm/systemone/main.py Normal file
View file

@ -0,0 +1,107 @@
from collections.abc import Mapping
from typing import Final
import httpx
from litellm.decisions.call import (
DecisionsCall,
SystemOneQuestions,
SystemOneRequestFields,
asend,
prepare_call,
reject_other_format,
send,
)
from litellm.types.decisions import DecisionsJSON, DecisionsResponse
from litellm.utils import client # pyright: ignore[reportUnknownVariableType] # client is an untyped decorator
def _prepare_systemone_call(
*,
model: str,
state: DecisionsJSON | None,
questions: SystemOneQuestions | None,
safety_identifier: str | None,
api_key: str | None,
api_base: str | None,
timeout: float | httpx.Timeout | None,
custom_llm_provider: str | None,
extra_headers: Mapping[str, str] | None,
kwargs: Mapping[str, object],
) -> DecisionsCall:
reject_other_format(model=model, kwargs=kwargs, field_name="input", other_function="decisions")
return prepare_call(
model=model,
fields=SystemOneRequestFields(state=state, questions=questions),
safety_identifier=safety_identifier,
api_key=api_key,
api_base=api_base,
timeout=timeout,
custom_llm_provider=custom_llm_provider,
extra_headers=extra_headers,
kwargs=kwargs,
)
def _systemone_response(response: object) -> DecisionsResponse:
assert isinstance(response, DecisionsResponse)
return response
@client
async def asystemone(
model: str,
state: DecisionsJSON | None = None,
questions: SystemOneQuestions | None = None,
api_key: str | None = None,
api_base: str | None = None,
timeout: float | httpx.Timeout | None = None,
custom_llm_provider: str | None = None,
extra_headers: Mapping[str, str] | None = None,
safety_identifier: str | None = None,
**kwargs: object, # kwargs-ok: litellm_params forwarded by @client
) -> DecisionsResponse:
call: Final = _prepare_systemone_call(
model=model,
state=state,
questions=questions,
safety_identifier=safety_identifier,
api_key=api_key,
api_base=api_base,
timeout=timeout,
custom_llm_provider=custom_llm_provider,
extra_headers=extra_headers,
kwargs=kwargs,
)
return _systemone_response(await asend(call))
@client
def systemone(
model: str,
state: DecisionsJSON | None = None,
questions: SystemOneQuestions | None = None,
api_key: str | None = None,
api_base: str | None = None,
timeout: float | httpx.Timeout | None = None,
custom_llm_provider: str | None = None,
extra_headers: Mapping[str, str] | None = None,
safety_identifier: str | None = None,
**kwargs: object, # kwargs-ok: litellm_params forwarded by @client
) -> DecisionsResponse:
call: Final = _prepare_systemone_call(
model=model,
state=state,
questions=questions,
safety_identifier=safety_identifier,
api_key=api_key,
api_base=api_base,
timeout=timeout,
custom_llm_provider=custom_llm_provider,
extra_headers=extra_headers,
kwargs=kwargs,
)
return _systemone_response(send(call))
__all__ = ["asystemone", "systemone"]

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

@ -489,6 +489,8 @@ class CallTypes(str, Enum):
asearch = "asearch"
decisions = "decisions"
adecisions = "adecisions"
systemone = "systemone"
asystemone = "asystemone"
arealtime = "_arealtime"
aresponses_websocket = "_aresponses_websocket"
create_batch = "create_batch"
@ -677,6 +679,8 @@ CallTypesLiteral = Literal[
"asearch",
"decisions",
"adecisions",
"systemone",
"asystemone",
"_arealtime",
"_aresponses_websocket",
"create_batch",
@ -788,8 +792,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],
"/systemone": [CallTypes.asystemone, CallTypes.systemone],
"/v1/systemone": [CallTypes.asystemone, CallTypes.systemone],
# Batches
"/batches": [CallTypes.acreate_batch, CallTypes.create_batch],
"/v1/batches": [CallTypes.acreate_batch, CallTypes.create_batch],

View file

@ -974,6 +974,12 @@ def _ocr_document_summary(document: object) -> str:
# Runs once per call to check if the user wants to send their data anywhere - PostHog/Sentry/Slack/etc.
_DECISION_CALL_TYPES: Final[frozenset[str]] = frozenset(
(CallTypes.decisions.value, CallTypes.adecisions.value, CallTypes.systemone.value, CallTypes.asystemone.value)
)
def function_setup(
original_function: str,
rules_obj: Rules,
@ -1220,7 +1226,7 @@ def function_setup(
if isinstance(search_query, list)
else search_query
)
elif call_type in (CallTypes.decisions.value, CallTypes.adecisions.value):
elif call_type in _DECISION_CALL_TYPES:
decisions_state: Final = args[1] if len(args) > 1 else kwargs.get("state") or kwargs.get("input") or ""
messages = decisions_state if isinstance(decisions_state, str) else json.dumps(decisions_state, default=str)
elif call_type in (CallTypes.image_edit.value, CallTypes.aimage_edit.value):
@ -8989,6 +8995,10 @@ class ProviderConfigManager:
return litellm.CloudflareDecisionsConfig()
if provider == LlmProviders.STRANDS_DECIDER:
return litellm.StrandsDeciderDecisionsConfig()
if provider == LlmProviders.DATABRICKS:
return litellm.DatabricksDecisionsConfig()
if provider == LlmProviders.HOSTED_VLLM:
return litellm.HostedVLLMDecisionsConfig()
if provider == LlmProviders.OPENAI:
return litellm.OpenAIDecisionsConfig()
if provider == LlmProviders.AZURE_AI:

View file

@ -21165,6 +21165,18 @@
"output_vector_size": 1024,
"source": "https://www.databricks.com/product/pricing/foundation-model-serving"
},
"databricks/databricks-openjev-qwen35-4b": {
"cache_creation_input_token_cost": 0.0,
"cache_read_input_token_cost": 0.0,
"input_cost_per_token": 0.0,
"litellm_provider": "databricks",
"metadata": {
"notes": "Pay-per-token Foundation Model API decision endpoint (OpenJev, Qwen 3.5 4B) queried through the System One API. Databricks published no DBU rate for it as of 2026-10-07, so a deployment's model_info prices set the billed cost."
},
"mode": "evaluation",
"output_cost_per_token": 0.0,
"source": "https://docs.databricks.com/aws/en/machine-learning/foundation-model-apis/supported-models"
},
"dataforseo/search": {
"input_cost_per_query": 0.003,
"litellm_provider": "dataforseo",

View file

@ -1370,7 +1370,8 @@
"rerank": true,
"a2a": true,
"interactions": true,
"video_generations": true
"video_generations": true,
"systemone": true
}
},
"huggingface": {

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

@ -1,5 +1,7 @@
from __future__ import annotations
import contextlib
import socket
import threading
from collections.abc import Generator, Mapping
from contextlib import contextmanager
@ -69,3 +71,47 @@ def refusing_forward_proxy(port: int = 0) -> Generator[ForwardProxy, None, None]
thread.join(timeout=6)
assert not thread.is_alive(), "Owned forward proxy survived cleanup"
server.server_close()
def _pipe(source: socket.socket, sink: socket.socket) -> None:
with contextlib.suppress(OSError):
for chunk in iter(lambda: source.recv(65536), b""):
sink.sendall(chunk)
with contextlib.suppress(OSError):
sink.shutdown(socket.SHUT_WR)
@contextmanager
def tunnelling_forward_proxy(destination_port: int, port: int = 0) -> Generator[ForwardProxy, None, None]:
received: Final[SimpleQueue[Tunnel]] = SimpleQueue()
class Handler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
rbufsize = 0
timeout = 30
request: socket.socket
def do_CONNECT(self) -> None:
received.put(Tunnel(self.command, self.path, {name.lower(): value for name, value in self.headers.items()}))
self.close_connection = True
with socket.create_connection(("127.0.0.1", destination_port), timeout=30) as destination:
self.send_response(200, "Connection established")
self.end_headers()
outbound: Final = threading.Thread(target=_pipe, args=(self.request, destination))
outbound.start()
_pipe(destination, self.request)
outbound.join(timeout=35)
def log_message(self, format: str, *args: object) -> None:
pass
with ThreadingHTTPServer(("127.0.0.1", port), Handler) as server:
thread: Final = threading.Thread(target=server.serve_forever, kwargs={"poll_interval": 0.05})
thread.start()
try:
yield ForwardProxy(f"http://127.0.0.1:{server.server_port}", server.server_port, received)
finally:
server.shutdown()
thread.join(timeout=6)
assert not thread.is_alive(), "Owned forward proxy survived cleanup"
server.server_close()

View file

@ -0,0 +1,334 @@
import asyncio
import json
import uuid
from collections.abc import Callable, Mapping
from typing import Final
import httpx
from integration._support.client import Gateway, Scenario, eventually, object_value, string_value
from integration._support.database import read_rows
from integration._support.openai_wire import answering_model_discovery, chat_reply
from integration._support.wire import Reply, Request, Wire, wire_server
from pydantic import JsonValue
_ENDPOINT: Final = "databricks-openjev-qwen35-4b"
_SECOND_ENDPOINT: Final = "databricks-openjev-qwen35-4b-canary"
_API_KEY: Final = "synthetic-databricks-key"
_BARE_NAME_RULE: Final = (
"must be the bare endpoint name (letters, digits, '-', '_' and '.', with no '/', '?', '#', spaces, or a "
"leading '.'), e.g. databricks-openjev-qwen35-4b"
)
_REJECTED_AT_LOAD: Final = "The router would drop this deployment at load time, so the write is rejected instead."
_MODEL_REQUIRED: Final = (
"opensource_classifier_config.model is required for provider 'databricks': the serving endpoint name, "
"e.g. databricks-openjev-qwen35-4b"
)
_BASE_NEEDS_KEY: Final = (
"opensource_classifier_config.api_base requires opensource_classifier_config.api_key: DATABRICKS_API_KEY or "
"DATABRICKS_TOKEN is only sent to DATABRICKS_API_BASE"
)
_BLANK_KEY: Final = (
"opensource_classifier_config.api_key must be non-empty; omit it to use the provider environment key"
)
_CLASSIFIER_ANSWER: Final[dict[str, JsonValue]] = {
"answers": {
"tier": {
"type": "choice",
"choice": "COMPLEX",
"confidence": 0.91,
"probabilities": {"SIMPLE": 0.03, "MEDIUM": 0.06, "COMPLEX": 0.91},
}
},
"usage": {"input_tokens": 12, "output_tokens": 1},
}
_ROWS_QUERY: Final = (
"SELECT request_id, status, metadata->>'internal_call_origin' AS origin "
'FROM "LiteLLM_SpendLogs" WHERE model_group = %s'
)
def _classifier(request: Request) -> Reply:
return Reply(body=json.dumps(_CLASSIFIER_ANSWER).encode())
def _tier(text: str) -> Callable[[Request], Reply]:
def respond(request: Request) -> Reply:
body: Final = json.loads(request.body)
return chat_reply(f"{text}-{uuid.uuid4().hex[:8]}", "tier-model", text, stream=bool(body.get("stream")))
return answering_model_discovery(respond)
def _classifier_config(api_base: str, **overrides: JsonValue) -> dict[str, JsonValue]:
return {
"provider": "databricks",
"model": _ENDPOINT,
"api_base": api_base,
"api_key": _API_KEY,
"timeout_ms": 20000,
"circuit_breaker_enabled": False,
**overrides,
}
def _without(config: Mapping[str, JsonValue], field: str) -> dict[str, JsonValue]:
return {name: value for name, value in config.items() if name != field}
def _router_config(classifier: Mapping[str, JsonValue], simple: str, complex_: str) -> dict[str, JsonValue]:
return {
"classifier_type": "oss_classifier",
"opensource_classifier_config": dict(classifier),
"tiers": {"SIMPLE": simple, "MEDIUM": simple, "COMPLEX": complex_, "REASONING": complex_},
}
def _validate(gateway: Gateway, config: Mapping[str, JsonValue]) -> dict[str, JsonValue]:
response: Final = gateway.request(
"POST", "/auto_router/validate_complexity_router_config", {"complexity_router_config": dict(config)}
)
assert response.status_code == 200, response.text
return object_value(response.json())
def _create_router(scenario: Scenario, config: Mapping[str, JsonValue]) -> tuple[str, str]:
name: Final = f"router-{uuid.uuid4().hex[:10]}"
created: Final = scenario.gateway.post(
"/model/new",
{
"model_name": name,
"litellm_params": {"model": "auto_router/complexity_router", "complexity_router_config": dict(config)},
"model_info": {},
},
)
identity: Final = string_value(object_value(created["model_info"])["id"])
scenario.cleanups.callback(scenario.delete_model, identity)
return name, identity
def _stored_classifier(gateway: Gateway, identity: str) -> dict[str, JsonValue]:
stored: Final = gateway.request("GET", "/model/info", params={"litellm_model_id": identity})
assert stored.status_code == 200, stored.text
(entry,) = stored.json()["data"]
router_config: Final = object_value(object_value(object_value(entry)["litellm_params"])["complexity_router_config"])
return object_value(router_config["opensource_classifier_config"])
def _judge_targets(judge: Wire) -> tuple[str, ...]:
return tuple(request.target for request in judge.drain() if request.method == "POST")
def _chat_body(model: str) -> dict[str, JsonValue]:
return {"model": model, "messages": [{"role": "user", "content": f"design a distributed cache {uuid.uuid4().hex}"}]}
async def _burst(base_url: str, key: str, model: str, count: int) -> tuple[httpx.Response, ...]:
async with httpx.AsyncClient(
base_url=base_url, timeout=60, trust_env=False, headers={"Authorization": f"Bearer {key}"}
) as client:
return tuple(
await asyncio.gather(*(client.post("/v1/chat/completions", json=_chat_body(model)) for _ in range(count)))
)
def _rows(router: str, *, want: int) -> tuple[dict[str, JsonValue], ...]:
return tuple(eventually(lambda: read_rows(_ROWS_QUERY, (router,)), lambda found: len(found) >= want, seconds=70))
def test_validate_accepts_a_databricks_classifier_with_a_bare_endpoint_name_of_any_length(gateway: Gateway) -> None:
config: Final = _router_config(
_classifier_config("https://dbc-example.cloud.databricks.com/serving-endpoints"), "simple", "complex"
)
assert _validate(gateway, config) == {"valid": True, "error": None}
long_name: Final = _router_config(
_classifier_config("https://dbc-example.cloud.databricks.com/serving-endpoints", model="e" * 5000),
"simple",
"complex",
)
assert _validate(gateway, long_name) == {"valid": True, "error": None}
def test_validate_names_each_databricks_classifier_misconfiguration(gateway: Gateway) -> None:
base: Final = _classifier_config("https://dbc-example.cloud.databricks.com/serving-endpoints")
cases: Final[tuple[tuple[str, dict[str, JsonValue], str], ...]] = (
("model omitted", _without(base, "model"), f"at opensource_classifier_config: Value error, {_MODEL_REQUIRED}"),
(
"api_base without api_key",
_without(base, "api_key"),
f"at opensource_classifier_config: Value error, {_BASE_NEEDS_KEY}",
),
("slash in the name", {**base, "model": "a/b"}, f"Databricks serving endpoint name 'a/b' {_BARE_NAME_RULE}"),
("space in the name", {**base, "model": "a b"}, f"Databricks serving endpoint name 'a b' {_BARE_NAME_RULE}"),
("leading dot", {**base, "model": ".hidden"}, f"Databricks serving endpoint name '.hidden' {_BARE_NAME_RULE}"),
("empty name", {**base, "model": ""}, f"Databricks serving endpoint name '' {_BARE_NAME_RULE}"),
(
"blank api_key",
{**base, "api_key": ""},
f"at opensource_classifier_config.api_key: Value error, {_BLANK_KEY}",
),
("int name", {**base, "model": 5}, "at opensource_classifier_config.model: Input should be a valid string"),
(
"list name",
{**base, "model": ["a"]},
"at opensource_classifier_config.model: Input should be a valid string",
),
("null name", {**base, "model": None}, "at opensource_classifier_config.model: Input should be a valid string"),
)
for label, classifier, expected in cases:
verdict: Final = _validate(gateway, _router_config(classifier, "simple", "complex"))
assert verdict["valid"] is False, (label, verdict)
error: Final = string_value(verdict["error"])
assert error.startswith("complexity_router_config is invalid "), (label, error)
assert expected in error, (label, error)
assert error.endswith(_REJECTED_AT_LOAD), (label, error)
def test_test_routing_classifies_through_the_databricks_endpoint_without_calling_the_tier_it_picked(
gateway: Gateway,
) -> None:
prompt: Final = f"design a distributed cache {uuid.uuid4().hex}"
with (
wire_server(_classifier) as judge,
wire_server(_tier("simple answer")) as simple,
wire_server(_tier("complex answer")) as complex_tier,
gateway.scenario() as scenario,
):
simple_model: Final = scenario.model(model="openai/simple-tier", api_base=simple.url)
complex_model: Final = scenario.model(model="openai/complex-tier", api_base=complex_tier.url)
config: Final = _router_config(_classifier_config(judge.url), simple_model, complex_model)
response: Final = gateway.request(
"POST", "/auto_router/test_routing", {"prompt": prompt, "complexity_router_config": config}
)
assert response.status_code == 200, response.text
assert response.json() == {
"routed_model": complex_model,
"routed_model_configured": True,
"routing_decision": {
"router_model_name": "auto_router_routing_test",
"router_type": "complexity",
"routed_model": complex_model,
"cause": "jev_classifier",
"tier": "COMPLEX",
"signals": [
"databricks-classifier:COMPLEX",
"databricks-confidence=0.910000",
"tier-probability:SIMPLE=0.030000",
"tier-probability:MEDIUM=0.060000",
"tier-probability:COMPLEX=0.910000",
],
"classifier_model": f"databricks/{_ENDPOINT}",
"classifier_cost": 0.0,
"classifier_probabilities": {"SIMPLE": 0.03, "MEDIUM": 0.06, "COMPLEX": 0.91},
"classifier_confidence": 0.91,
"conversation_continuing": False,
},
}, response.text
(call,) = [request for request in judge.drain() if request.method == "POST"]
assert call.target == f"/{_ENDPOINT}/invocations", call.target
assert call.headers.get("authorization") == f"Bearer {_API_KEY}", call.headers
body: Final = json.loads(call.body)
assert body["model"] == _ENDPOINT, body
assert body["state"] == f"\nClassify this message:\n{prompt}", body
assert _judge_targets(simple) == () and _judge_targets(complex_tier) == ()
def test_model_new_refuses_a_databricks_classifier_without_an_endpoint_name_and_stores_nothing(
gateway: Gateway,
) -> None:
name: Final = f"router-{uuid.uuid4().hex[:10]}"
config: Final = _router_config(
_without(_classifier_config("https://dbc-example.cloud.databricks.com/serving-endpoints"), "model"),
"simple",
"complex",
)
response: Final = gateway.request(
"POST",
"/model/new",
{
"model_name": name,
"litellm_params": {"model": "auto_router/complexity_router", "complexity_router_config": config},
"model_info": {},
},
)
assert response.status_code == 400, response.text
error: Final = object_value(object_value(response.json())["error"])
assert _MODEL_REQUIRED in string_value(error["message"]), error
assert (error["type"], error["param"]) == ("validation_error", "litellm_params.model"), error
listed: Final = gateway.get("/model/info")["data"]
assert isinstance(listed, list)
assert [entry for entry in map(object_value, listed) if entry["model_name"] == name] == []
def test_model_update_refuses_dropping_the_endpoint_name_and_keeps_the_stored_classifier_serving(
gateway: Gateway,
) -> None:
with (
wire_server(_classifier) as judge,
wire_server(_tier("simple answer")) as simple,
wire_server(_tier("complex answer")) as complex_tier,
gateway.scenario() as scenario,
):
simple_model: Final = scenario.model(model="openai/simple-tier", api_base=simple.url)
complex_model: Final = scenario.model(model="openai/complex-tier", api_base=complex_tier.url)
classifier: Final = _classifier_config(judge.url)
name, identity = _create_router(scenario, _router_config(classifier, simple_model, complex_model))
dropped: Final = _router_config(_without(classifier, "model"), simple_model, complex_model)
response: Final = gateway.request(
"PATCH",
f"/model/{identity}/update",
{"litellm_params": {"model": "auto_router/complexity_router", "complexity_router_config": dropped}},
)
assert response.status_code == 400, response.text
assert _MODEL_REQUIRED in string_value(object_value(object_value(response.json())["error"])["message"]), (
response.text
)
assert _stored_classifier(gateway, identity)["model"] == _ENDPOINT
chat: Final = gateway.request("POST", "/v1/chat/completions", _chat_body(name))
assert chat.status_code == 200, chat.text
assert chat.json()["choices"][0]["message"]["content"] == "complex answer", chat.text
assert chat.headers["x-litellm-complexity-router-cause"] == "jev_classifier", dict(chat.headers)
assert _judge_targets(judge) == (f"/{_ENDPOINT}/invocations",)
async def test_renaming_the_endpoint_under_a_burst_moves_the_classifier_and_logs_every_request_once(
gateway: Gateway,
) -> None:
base_url: Final = str(gateway.client.base_url)
with (
wire_server(_classifier) as judge,
wire_server(_tier("simple answer")) as simple,
wire_server(_tier("complex answer")) as complex_tier,
gateway.scenario() as scenario,
):
simple_model: Final = scenario.model(model="openai/simple-tier", api_base=simple.url)
complex_model: Final = scenario.model(model="openai/complex-tier", api_base=complex_tier.url)
classifier: Final = _classifier_config(judge.url)
name, identity = _create_router(scenario, _router_config(classifier, simple_model, complex_model))
burst: Final = asyncio.create_task(_burst(base_url, gateway.key, name, 24))
renamed: Final = _router_config({**classifier, "model": _SECOND_ENDPOINT}, simple_model, complex_model)
patched: Final = gateway.request(
"PATCH",
f"/model/{identity}/update",
{"litellm_params": {"model": "auto_router/complexity_router", "complexity_router_config": renamed}},
)
assert patched.status_code == 200, patched.text
during: Final = await burst
assert [response.status_code for response in during] == [200] * 24, [response.text for response in during]
assert set(_judge_targets(judge)) <= {f"/{_ENDPOINT}/invocations", f"/{_SECOND_ENDPOINT}/invocations"}
assert _stored_classifier(gateway, identity)["model"] == _SECOND_ENDPOINT
def classifier_target() -> tuple[str, ...]:
chat: Final = gateway.request("POST", "/v1/chat/completions", _chat_body(name))
assert chat.status_code == 200, chat.text
return _judge_targets(judge)
eventually(classifier_target, lambda targets: targets == (f"/{_SECOND_ENDPOINT}/invocations",), seconds=30)
after: Final = await _burst(base_url, gateway.key, name, 6)
assert [response.status_code for response in after] == [200] * 6, [response.text for response in after]
assert set(_judge_targets(judge)) == {f"/{_SECOND_ENDPOINT}/invocations"}
identities: Final = tuple(string_value(response.json()["id"]) for response in (*during, *after))
assert len(set(identities)) == len(identities), identities
rows: Final = _rows(name, want=len(identities))
request_rows: Final = [row for row in rows if row["origin"] != "autorouter_classifier"]
assert sorted(string_value(row["request_id"]) for row in request_rows) == sorted(identities), request_rows
assert all(row["status"] == "success" for row in rows), rows

View file

@ -24,6 +24,16 @@ _CONFIGURED_PROBE_REPLY: Final = JsonResponse(
"usage": {"input_tokens": 10, "output_tokens": 1},
},
)
_VLLM_PROBE_REPLY: Final = JsonResponse(
content_type="application/json",
body={
"model": "Qwen/Qwen3-0.6B",
"answers": {
"reachable": {"type": "choice", "choice": "yes", "confidence": 1.0, "probabilities": {"yes": 1.0, "no": 0.0}}
},
"usage": {"input_tokens": 10, "output_tokens": 1},
},
)
_STRANDS_PROBE_REPLY: Final = JsonResponse(
content_type="application/json",
body={
@ -147,3 +157,72 @@ def test_evaluation_mode_health_check_of_the_self_hosted_strands_model_resolves_
},
)
]
def test_evaluation_mode_health_check_of_hosted_vllm_sends_a_choice_probe(gateway: Gateway) -> None:
with gateway.scenario() as scenario:
handle: Final = register_scenario(f"health-decisions-{uuid.uuid4().hex[:12]}", _VLLM_PROBE_REPLY)
scenario.cleanups.callback(delete_scenario, handle)
model: Final = scenario.model(
model="hosted_vllm/Qwen/Qwen3-0.6B",
api_base=handle.api_base(),
model_info={"mode": "evaluation"},
)
report: Final = _health_report(gateway, model)
assert (report["healthy_count"], report["unhealthy_count"]) == (1, 0), report
assert _probes_sent_to(gateway, handle) == [
(
f"/{handle.scenario_id}/v1/systemone",
{
"model": "Qwen/Qwen3-0.6B",
"state": os.environ.get("DEFAULT_HEALTH_CHECK_PROMPT", "test from litellm"),
"questions": {
"reachable": {
"type": "choice",
"instructions": "Is the service reachable?",
"criteria": {"yes": None, "no": None},
}
},
},
)
]
_DATABRICKS_PROBE_REPLY: Final = JsonResponse(
content_type="application/json",
body={
"model": "databricks-openjev-qwen35-4b",
"answers": {"reachable": {"type": "noul", "noul": 1.0}},
"usage": {"input_tokens": 10, "output_tokens": 1},
},
)
def test_evaluation_mode_health_check_of_a_databricks_serving_endpoint_resolves_the_mode_from_the_cost_map(
gateway: Gateway,
) -> None:
with gateway.scenario() as scenario:
handle: Final = register_scenario(f"health-decisions-{uuid.uuid4().hex[:12]}", _DATABRICKS_PROBE_REPLY)
scenario.cleanups.callback(delete_scenario, handle)
model: Final = scenario.model(
model="databricks/databricks-openjev-qwen35-4b",
api_base=handle.api_base(),
api_key="synthetic-databricks-key",
)
listed: Final = gateway.request("GET", "/v2/model/info", params={"model": model})
assert listed.status_code == 200, listed.text
assert [object_value(object_value(entry)["model_info"])["mode"] for entry in listed.json()["data"]] == [
"evaluation"
], listed.text
report: Final = _health_report(gateway, model)
assert (report["healthy_count"], report["unhealthy_count"]) == (1, 0), report
assert _probes_sent_to(gateway, handle) == [
(
f"/{handle.scenario_id}/databricks-openjev-qwen35-4b/invocations",
{
"model": "databricks-openjev-qwen35-4b",
"state": os.environ.get("DEFAULT_HEALTH_CHECK_PROMPT", "test from litellm"),
"questions": {"reachable": {"type": "noul", "instructions": "Is the service reachable?"}},
},
)
]

View file

@ -0,0 +1,518 @@
import threading
import time
import uuid
from collections.abc import Mapping, Sequence
from pathlib import Path
from typing import Final
import httpx
import pytest
from integration._support.client import Gateway, Scenario, eventually, object_value, string_value
from integration._support.database import read_rows
from integration._support.process import graceful_stop_seconds, owned_proxy_process
from integration._support.upstream import ScenarioHandle, delete_scenario, register_scenario
from integration.cost_calculation.cost_tracking_case import JsonResponse
from pydantic import JsonValue
_ENDPOINT: Final = "databricks-openjev-qwen35-4b"
_API_KEY: Final = "synthetic-databricks-key"
_ENV_KEY: Final = "synthetic-databricks-env-key"
_ENV_TOKEN: Final = "synthetic-databricks-env-token"
_STATE: Final[dict[str, JsonValue]] = {"ticket": "The export job hangs at 99%", "component": "billing"}
_TIER_CRITERIA: Final[dict[str, JsonValue]] = {"SIMPLE": "a lookup", "MEDIUM": "some work", "COMPLEX": "deep work"}
_QUESTIONS: Final[dict[str, JsonValue]] = {"tier": {"type": "choice", "criteria": _TIER_CRITERIA}}
_OPENAI_INPUT: Final = "The export job hangs at 99% in billing"
_OPENAI_TIER_QUESTION: Final[dict[str, JsonValue]] = {
"type": "choice",
"name": "tier",
"instructions": "Which tier?",
"choices": [{"value": name, "description": description} for name, description in _TIER_CRITERIA.items()],
}
_ANSWERS: Final[dict[str, JsonValue]] = {
"tier": {
"type": "choice",
"choice": "COMPLEX",
"confidence": 0.91,
"probabilities": {"SIMPLE": 0.03, "MEDIUM": 0.06, "COMPLEX": 0.91},
}
}
_USAGE: Final[dict[str, JsonValue]] = {"input_tokens": 367, "output_tokens": 3}
_ANSWER_BODY: Final[dict[str, JsonValue]] = {"model": _ENDPOINT, "answers": _ANSWERS, "usage": _USAGE}
_BARE_NAME_RULE: Final = (
"must be the bare endpoint name (letters, digits, '-', '_' and '.', with no '/', '?', '#', spaces, or a "
"leading '.'), e.g. databricks-openjev-qwen35-4b"
)
_MISSING_KEY: Final = "Missing API key for Decisions provider 'databricks'"
_NON_BARE_NAMES: Final[tuple[tuple[str, str], ...]] = (
("slash", "foo/bar"),
("space", "a b"),
("leading-dot", ".hidden"),
("question-mark", "a?b"),
("hash", "a#b"),
)
_DATABRICKS_ERRORS: Final[tuple[tuple[int, dict[str, JsonValue]], ...]] = (
(400, {"error_code": "BAD_REQUEST", "message": "Invalid input: questions must not be empty"}),
(404, {"error_code": "RESOURCE_DOES_NOT_EXIST", "message": f"Endpoint with name '{_ENDPOINT}' does not exist."}),
)
_SPEND_QUERY: Final = (
'SELECT spend, status, call_type, model_group, custom_llm_provider, api_base FROM "LiteLLM_SpendLogs" '
"WHERE request_id = %s"
)
_OWNED_PROXY_BUDGET: Final = int(2 * graceful_stop_seconds() + 120)
_MEGABYTE_NAME: Final = "a" * 1_000_000 + "/"
def _number(value: JsonValue) -> float:
assert isinstance(value, (int, float)) and not isinstance(value, bool), value
return float(value)
def _register(scenario: Scenario, body: dict[str, JsonValue], *, status: int = 200) -> ScenarioHandle:
handle: Final = register_scenario(
f"databricks-{uuid.uuid4().hex[:12]}", JsonResponse(content_type="application/json", body=body, status=status)
)
scenario.cleanups.callback(delete_scenario, handle)
return handle
def _decide(gateway: Gateway, model: str, *, key: str | None = None) -> httpx.Response:
return gateway.request("POST", "/v1/systemone", {"model": model, "state": _STATE, "questions": _QUESTIONS}, key=key)
def _observed(gateway: Gateway) -> tuple[dict[str, JsonValue], ...]:
with httpx.Client(base_url=gateway.upstream_url, timeout=5, trust_env=False) as upstream:
return tuple(map(object_value, upstream.get("/__observations").json()["requests"]))
def _calls_to(requests: Sequence[dict[str, JsonValue]], handle: ScenarioHandle) -> list[dict[str, JsonValue]]:
return [request for request in requests if string_value(request["path"]).startswith(f"/{handle.scenario_id}/")]
def _upstream_calls(gateway: Gateway, handle: ScenarioHandle) -> list[dict[str, JsonValue]]:
return _calls_to(_observed(gateway), handle)
def _spend_row(call_id: str) -> dict[str, JsonValue]:
rows: Final = eventually(lambda: read_rows(_SPEND_QUERY, (call_id,)), lambda found: len(found) == 1, seconds=70)
return rows[0]
def _wildcard_deployment(scenario: Scenario, **parameters: JsonValue) -> str:
prefix: Final = f"databricks-{uuid.uuid4().hex[:8]}"
created: Final = scenario.gateway.post(
"/model/new",
{"model_name": f"{prefix}/*", "litellm_params": {"model": "databricks/*", **parameters}, "model_info": {}},
)
scenario.cleanups.callback(scenario.delete_model, string_value(object_value(created["model_info"])["id"]))
return prefix
def _router_config(classifier: Mapping[str, JsonValue], simple: str, complex_: str) -> dict[str, JsonValue]:
return {
"classifier_type": "oss_classifier",
"opensource_classifier_config": dict(classifier),
"tiers": {"SIMPLE": simple, "MEDIUM": simple, "COMPLEX": complex_, "REASONING": complex_},
}
def _environment_classifier() -> dict[str, JsonValue]:
return {"provider": "databricks", "model": _ENDPOINT, "timeout_ms": 20000, "circuit_breaker_enabled": False}
def _router(scenario: Scenario, classifier: Mapping[str, JsonValue]) -> str:
simple: Final = scenario.model(model="openai/simple-tier")
complex_: Final = scenario.model(model="openai/complex-tier")
name: Final = f"router-{uuid.uuid4().hex[:10]}"
created: Final = scenario.gateway.post(
"/model/new",
{
"model_name": name,
"litellm_params": {
"model": "auto_router/complexity_router",
"complexity_router_config": _router_config(classifier, simple, complex_),
},
"model_info": {},
},
)
scenario.cleanups.callback(scenario.delete_model, string_value(object_value(created["model_info"])["id"]))
return name
def _chat(gateway: Gateway, model: str, *, key: str | None = None) -> httpx.Response:
return gateway.request(
"POST",
"/v1/chat/completions",
{"model": model, "messages": [{"role": "user", "content": f"design a distributed cache {uuid.uuid4().hex}"}]},
key=key,
)
def _assert_classified_by_the_complex_tier(response: httpx.Response, router: str) -> None:
assert response.status_code == 200, response.text
assert response.json()["choices"][0]["message"]["content"], response.text
assert (
response.headers["x-litellm-model-group"],
response.headers["x-litellm-model-name"],
response.headers["x-litellm-complexity-router-tier"],
response.headers["x-litellm-complexity-router-cause"],
response.headers["x-litellm-classifier-cost"],
) == (router, "openai/complex-tier", "COMPLEX", "jev_classifier", "0.0"), dict(response.headers)
def _assert_classifier_call(
call: Mapping[str, JsonValue], *, path: str, authorization: str, prompt_marker: str
) -> None:
assert call["path"] == path, call
assert call["authorization"] == authorization, call
body: Final = object_value(call["body"])
assert body["model"] == _ENDPOINT, body
assert prompt_marker in string_value(body["state"]), body
assert sorted(object_value(object_value(body["questions"])["tier"])) == ["criteria", "instructions", "type"], body
def _assert_refused_before_any_upstream_call(gateway: Gateway, model: str, message: str) -> None:
with gateway.scenario() as scenario:
handle: Final = _register(scenario, _ANSWER_BODY)
deployment: Final = scenario.model(model=model, api_base=handle.api_base(), api_key=_API_KEY)
response: Final = _decide(gateway, deployment)
assert response.status_code == 400, response.text
assert message in response.text, response.text
assert _upstream_calls(gateway, handle) == []
row: Final = _spend_row(response.headers["x-litellm-call-id"])
assert (row["status"], row["call_type"], _number(row["spend"])) == ("failure", "asystemone", 0.0), row
@pytest.mark.parametrize("name", [name for _, name in _NON_BARE_NAMES], ids=[label for label, _ in _NON_BARE_NAMES])
def test_a_deployment_naming_a_non_bare_endpoint_is_refused_before_any_upstream_call(
gateway: Gateway, name: str
) -> None:
_assert_refused_before_any_upstream_call(
gateway, f"databricks/{name}", f"Databricks serving endpoint name {name!r} {_BARE_NAME_RULE}"
)
def test_a_deployment_naming_no_endpoint_is_refused_before_any_upstream_call(gateway: Gateway) -> None:
_assert_refused_before_any_upstream_call(gateway, "databricks/", "A model name is required for the Decisions API")
def test_an_openai_format_request_at_v1_decisions_reaches_the_serving_endpoint_as_system_one(gateway: Gateway) -> None:
with gateway.scenario() as scenario:
handle: Final = _register(scenario, _ANSWER_BODY)
deployment: Final = scenario.model(
model=f"databricks/{_ENDPOINT}", api_base=handle.api_base(), api_key=_API_KEY
)
response: Final = gateway.request(
"POST",
"/v1/decisions",
{
"model": deployment,
"input": _OPENAI_INPUT,
"questions": [_OPENAI_TIER_QUESTION],
},
)
assert response.status_code == 200, response.text
assert response.json() == {
"model": _ENDPOINT,
"answers": [
{
"type": "choice",
"name": "tier",
"choice": "COMPLEX",
"probabilities": [
{"value": "SIMPLE", "probability": 0.03},
{"value": "MEDIUM", "probability": 0.06},
{"value": "COMPLEX", "probability": 0.91},
],
"confidence": 0.91,
}
],
"usage": {
"input_tokens": 367,
"input_tokens_details": {"cached_tokens": 0, "cache_write_tokens": 0},
"output_tokens": 3,
"output_tokens_details": {"reasoning_tokens": 0},
"total_tokens": 370,
},
}, response.text
assert response.headers["x-litellm-model-name"] == f"databricks/{_ENDPOINT}", dict(response.headers)
(call,) = _upstream_calls(gateway, handle)
assert call["path"] == f"/{handle.scenario_id}/{_ENDPOINT}/invocations", call
assert call["authorization"] == f"Bearer {_API_KEY}", call
assert call["body"] == {
"model": _ENDPOINT,
"state": _OPENAI_INPUT,
"questions": {"tier": {"type": "choice", "instructions": "Which tier?", "criteria": _TIER_CRITERIA}},
}, call
row: Final = _spend_row(response.headers["x-litellm-call-id"])
assert (
row["status"],
row["call_type"],
row["custom_llm_provider"],
row["model_group"],
row["api_base"],
_number(row["spend"]),
) == ("success", "asystemone", "databricks", deployment, f"{handle.api_base()}/{_ENDPOINT}/invocations", 0.0), (
row
)
def test_a_wildcard_deployment_sends_each_request_to_the_named_endpoint_and_bills_each_once(gateway: Gateway) -> None:
with gateway.scenario() as scenario:
handle: Final = _register(scenario, _ANSWER_BODY)
prefix: Final = _wildcard_deployment(scenario, api_base=handle.api_base(), api_key=_API_KEY)
model: Final = f"{prefix}/{_ENDPOINT}"
responses: Final = tuple(_decide(gateway, model) for _ in range(2))
for response in responses:
assert response.status_code == 200, response.text
assert response.json() == _ANSWER_BODY, response.text
assert response.headers["x-litellm-model-group"] == model, dict(response.headers)
assert response.headers["x-litellm-model-name"] == f"databricks/{_ENDPOINT}", dict(response.headers)
call_ids: Final = tuple(response.headers["x-litellm-call-id"] for response in responses)
assert len(set(call_ids)) == 2, call_ids
calls: Final = _upstream_calls(gateway, handle)
assert len(calls) == 2, calls
for call in calls:
assert call["path"] == f"/{handle.scenario_id}/{_ENDPOINT}/invocations", call
assert call["authorization"] == f"Bearer {_API_KEY}", call
assert call["body"] == {"model": _ENDPOINT, "state": _STATE, "questions": _QUESTIONS}, call
for call_id in call_ids:
row: Final = _spend_row(call_id)
assert (
row["status"],
row["call_type"],
row["custom_llm_provider"],
row["model_group"],
row["api_base"],
_number(row["spend"]),
) == ("success", "asystemone", "databricks", model, f"{handle.api_base()}/{_ENDPOINT}/invocations", 0.0), (
row
)
@pytest.mark.parametrize("model", (5, ["a"]), ids=("int", "list"))
def test_a_non_string_model_is_refused_at_the_gateway_without_an_upstream_call(
gateway: Gateway, model: JsonValue
) -> None:
with gateway.scenario() as scenario:
handle: Final = _register(scenario, _ANSWER_BODY)
_wildcard_deployment(scenario, api_base=handle.api_base(), api_key=_API_KEY)
response: Final = gateway.request(
"POST", "/v1/systemone", {"model": model, "state": _STATE, "questions": _QUESTIONS}
)
assert 400 <= response.status_code < 500, response.text
assert _upstream_calls(gateway, handle) == []
def test_a_five_kilobyte_endpoint_name_reaches_the_upstream_and_its_not_found_answer_is_kept(gateway: Gateway) -> None:
name: Final = "n" * 5000
with gateway.scenario() as scenario:
handle: Final = _register(
scenario,
{"error_code": "RESOURCE_DOES_NOT_EXIST", "message": f"Endpoint with name '{name}' does not exist."},
status=404,
)
prefix: Final = _wildcard_deployment(scenario, api_base=handle.api_base(), api_key=_API_KEY)
response: Final = _decide(gateway, f"{prefix}/{name}")
assert response.status_code == 404, response.text[:500]
assert "RESOURCE_DOES_NOT_EXIST" in response.text, response.text[:500]
(call,) = _upstream_calls(gateway, handle)
assert call["path"] == f"/{handle.scenario_id}/{name}/invocations", call["path"][:200]
assert call["authorization"] == f"Bearer {_API_KEY}", call["authorization"]
assert call["body"] == {"model": name, "state": _STATE, "questions": _QUESTIONS}
row: Final = _spend_row(response.headers["x-litellm-call-id"])
assert (row["status"], row["call_type"], row["model_group"], _number(row["spend"])) == (
"failure",
"asystemone",
f"{prefix}/{name}",
0.0,
), row
@pytest.mark.parametrize("status,body", _DATABRICKS_ERRORS, ids=("400", "404"))
def test_a_databricks_error_keeps_its_status_and_message_and_bills_nothing(
gateway: Gateway, status: int, body: dict[str, JsonValue]
) -> None:
with gateway.scenario() as scenario:
handle: Final = _register(scenario, body, status=status)
model: Final = scenario.model(model=f"databricks/{_ENDPOINT}", api_base=handle.api_base(), api_key=_API_KEY)
response: Final = _decide(gateway, model)
assert response.status_code == status, response.text
assert string_value(body["message"]) in response.text, response.text
(call,) = _upstream_calls(gateway, handle)
assert call["path"] == f"/{handle.scenario_id}/{_ENDPOINT}/invocations", call
row: Final = _spend_row(response.headers["x-litellm-call-id"])
assert (row["status"], row["call_type"], row["model_group"], _number(row["spend"])) == (
"failure",
"asystemone",
model,
0.0,
), row
def test_a_deployment_without_a_key_and_no_environment_key_is_refused_before_any_upstream_call(
gateway: Gateway,
) -> None:
with gateway.scenario() as scenario:
handle: Final = _register(scenario, _ANSWER_BODY)
model: Final = scenario.model(model=f"databricks/{_ENDPOINT}", api_base=handle.api_base(), api_key=None)
response: Final = _decide(gateway, model)
assert response.status_code == 401, response.text
assert _MISSING_KEY in response.text, response.text
assert _upstream_calls(gateway, handle) == []
@pytest.mark.timeout(_OWNED_PROXY_BUDGET)
def test_the_environment_key_and_base_serve_a_deployment_a_classifier_and_a_team_member_that_leave_them_unset(
gateway: Gateway, tmp_path: Path
) -> None:
with gateway.scenario() as rig_scenario:
handle: Final = _register(rig_scenario, _ANSWER_BODY)
environment_base: Final = f"{handle.api_base()}/serving-endpoints"
environment_path: Final = f"/{handle.scenario_id}/serving-endpoints/{_ENDPOINT}/invocations"
environment: Final = {
"DATABRICKS_API_KEY": _ENV_KEY,
"DATABRICKS_TOKEN": _ENV_TOKEN,
"DATABRICKS_API_BASE": environment_base,
}
with owned_proxy_process(gateway, tmp_path, environment) as owned, owned.gateway.scenario() as scenario:
candidate: Final = owned.gateway
with_base: Final = scenario.model(model=f"databricks/{_ENDPOINT}", api_base=handle.api_base(), api_key=None)
bare: Final = scenario.model(model=f"databricks/{_ENDPOINT}", api_base=None, api_key=None)
decided: Final = tuple(_decide(candidate, model) for model in (with_base, bare))
for response in decided:
assert response.status_code == 200, response.text
assert response.json() == _ANSWER_BODY, response.text
assert [(call["path"], call["authorization"]) for call in _upstream_calls(candidate, handle)] == [
(f"/{handle.scenario_id}/{_ENDPOINT}/invocations", f"Bearer {_ENV_KEY}"),
(environment_path, f"Bearer {_ENV_KEY}"),
]
for response, model in zip(decided, (with_base, bare)):
row: Final = _spend_row(response.headers["x-litellm-call-id"])
assert (row["status"], row["model_group"], _number(row["spend"])) == ("success", model, 0.0), row
assert (
_spend_row(decided[1].headers["x-litellm-call-id"])["api_base"]
== f"{environment_base}/{_ENDPOINT}/invocations"
)
router: Final = _router(scenario, _environment_classifier())
routed: Final = _chat(candidate, router)
_assert_classified_by_the_complex_tier(routed, router)
(judge_call,) = _upstream_calls(candidate, handle)
_assert_classifier_call(
judge_call,
path=environment_path,
authorization=f"Bearer {_ENV_KEY}",
prompt_marker="design a distributed cache",
)
team: Final = scenario.team(team_member_permissions=["/auto_router/manage"])
member: Final = scenario.member(team)
member_key: Final = scenario.key(user_id=member, team_id=team)
simple: Final = scenario.model(model="openai/simple-tier")
complex_: Final = scenario.model(model="openai/complex-tier")
team_router: Final = f"team-router-{uuid.uuid4().hex[:8]}"
created: Final = candidate.request(
"POST",
"/model/new",
{
"model_name": team_router,
"litellm_params": {
"model": "auto_router/complexity_router",
"complexity_router_config": _router_config(_environment_classifier(), simple, complex_),
},
"model_info": {"team_id": team},
},
key=member_key,
)
assert created.status_code == 200, created.text
team_router_id: Final = string_value(object_value(object_value(created.json())["model_info"])["id"])
scenario.cleanups.callback(scenario.delete_model, team_router_id)
stored: Final = candidate.request("GET", "/model/info", params={"litellm_model_id": team_router_id})
assert stored.status_code == 200, stored.text
(entry,) = stored.json()["data"]
stored_classifier: Final = object_value(
object_value(object_value(object_value(entry)["litellm_params"])["complexity_router_config"])[
"opensource_classifier_config"
]
)
assert (stored_classifier["provider"], stored_classifier["model"]) == ("databricks", _ENDPOINT), entry
assert "api_key" not in stored_classifier and "api_base" not in stored_classifier, entry
member_chat: Final = _chat(candidate, team_router, key=member_key)
_assert_classified_by_the_complex_tier(member_chat, team_router)
(member_judge_call,) = _upstream_calls(candidate, handle)
_assert_classifier_call(
member_judge_call,
path=environment_path,
authorization=f"Bearer {_ENV_KEY}",
prompt_marker="design a distributed cache",
)
@pytest.mark.timeout(_OWNED_PROXY_BUDGET)
def test_the_environment_token_serves_a_deployment_and_a_classifier_when_no_environment_key_is_set(
gateway: Gateway, tmp_path: Path
) -> None:
with gateway.scenario() as rig_scenario:
handle: Final = _register(rig_scenario, _ANSWER_BODY)
environment_path: Final = f"/{handle.scenario_id}/serving-endpoints/{_ENDPOINT}/invocations"
environment: Final = {
"DATABRICKS_TOKEN": _ENV_TOKEN,
"DATABRICKS_API_BASE": f"{handle.api_base()}/serving-endpoints",
}
with (
owned_proxy_process(gateway, tmp_path, environment, remove_environment=("DATABRICKS_API_KEY",)) as owned,
owned.gateway.scenario() as scenario,
):
candidate: Final = owned.gateway
bare: Final = scenario.model(model=f"databricks/{_ENDPOINT}", api_base=None, api_key=None)
decided: Final = _decide(candidate, bare)
assert decided.status_code == 200, decided.text
assert decided.json() == _ANSWER_BODY, decided.text
(decision_call,) = _upstream_calls(candidate, handle)
assert (decision_call["path"], decision_call["authorization"]) == (environment_path, f"Bearer {_ENV_TOKEN}")
assert _spend_row(decided.headers["x-litellm-call-id"])["status"] == "success"
router: Final = _router(scenario, _environment_classifier())
routed: Final = _chat(candidate, router)
_assert_classified_by_the_complex_tier(routed, router)
(judge_call,) = _upstream_calls(candidate, handle)
_assert_classifier_call(
judge_call,
path=environment_path,
authorization=f"Bearer {_ENV_TOKEN}",
prompt_marker="design a distributed cache",
)
def test_a_megabyte_endpoint_name_is_refused_in_linear_time_while_liveliness_stays_fast(gateway: Gateway) -> None:
with gateway.scenario() as scenario:
handle: Final = _register(scenario, _ANSWER_BODY)
prefix: Final = _wildcard_deployment(scenario, api_base=handle.api_base(), api_key=_API_KEY)
model: Final = f"{prefix}/{_MEGABYTE_NAME}"
anonymous: Final = gateway.client.post(
"/v1/systemone", json={"model": model, "state": _STATE, "questions": _QUESTIONS}
)
assert anonymous.status_code == 401, anonymous.text[:300]
liveliness: Final[list[tuple[int, float]]] = []
stop: Final = threading.Event()
def poll() -> None:
while not stop.is_set():
started: Final = time.perf_counter()
probe: Final = gateway.client.get("/health/liveliness")
liveliness.append((probe.status_code, time.perf_counter() - started))
poller: Final = threading.Thread(target=poll)
poller.start()
started: Final = time.perf_counter()
response: Final = _decide(gateway, model)
elapsed: Final = time.perf_counter() - started
stop.set()
poller.join()
assert elapsed < 3, elapsed
assert liveliness and all(status == 200 for status, _ in liveliness), liveliness
assert max(seconds for _, seconds in liveliness) < 1, liveliness
assert response.status_code == 400, response.text[:300]
assert _BARE_NAME_RULE in response.text, response.text[-400:]
assert _upstream_calls(gateway, handle) == []
row: Final = _spend_row(response.headers["x-litellm-call-id"])
assert (row["status"], row["call_type"], _number(row["spend"])) == ("failure", "asystemone", 0.0), row

View file

@ -0,0 +1,593 @@
import math
import uuid
from collections.abc import Callable, Sequence
from dataclasses import dataclass
from pathlib import Path
from typing import Final
import httpx
import pytest
import yaml
from integration._support.client import Gateway, Scenario, eventually, object_value, string_value
from integration._support.database import read_rows
from integration._support.process import owned_proxy_process
from integration._support.upstream import ScenarioHandle, delete_scenario, register_scenario
from integration.cost_calculation.cost_tracking_case import JsonResponse
from pydantic import JsonValue, TypeAdapter
import litellm
_API_KEY: Final = "synthetic-decisions-key"
_SAFETY_IDENTIFIER: Final = "end-user-7"
_DROPPING_DEPLOYMENT: Final = "decisions-under-litellm-settings-drop-params"
_JSON_OBJECT: Final = TypeAdapter(dict[str, JsonValue])
_INPUT: Final = "Ticket (billing): The export job hangs at 99%"
_FOLLOW_UP: Final = "Customer: still stuck after retrying"
_QUESTIONS: Final[list[JsonValue]] = [
{"type": "predicate", "name": "defect", "instructions": "Is this a defect?"},
{
"type": "choice",
"name": "severity",
"instructions": "How severe is it?",
"choices": [{"value": "low", "description": "cosmetic"}, {"value": "high", "description": "blocks users"}],
},
{
"type": "score",
"name": "confidence",
"instructions": "How sure are you?",
"levels": [{"label": "unsure"}, {"label": "sure"}],
},
]
_SYSTEM_ONE_QUESTIONS: Final[dict[str, JsonValue]] = {
"defect": {"type": "noul", "instructions": "Is this a defect?"},
"severity": {
"type": "choice",
"instructions": "How severe is it?",
"criteria": {"low": "cosmetic", "high": "blocks users"},
},
"confidence": {"type": "score", "instructions": "How sure are you?", "criteria": ["unsure", "sure"]},
}
_SYSTEM_ONE_ANSWERS: Final[dict[str, JsonValue]] = {
"defect": {"type": "noul", "noul": 0.93},
"severity": {"type": "choice", "choice": "high", "confidence": 0.8, "probabilities": {"low": 0.2, "high": 0.8}},
"confidence": {
"type": "score",
"score": 1.0,
"confidence": 0.7,
"legend": {"0": "unsure", "1": "sure"},
"probabilities": {"0": 0.3, "1": 0.7},
},
}
_ANSWERS: Final[list[JsonValue]] = [
{"type": "predicate", "name": "defect", "probability": 0.93},
{
"type": "choice",
"name": "severity",
"choice": "high",
"probabilities": [{"value": "low", "probability": 0.2}, {"value": "high", "probability": 0.8}],
"confidence": 0.8,
},
{
"type": "score",
"name": "confidence",
"score": 1.0,
"probabilities": [
{"value": 0, "label": "unsure", "probability": 0.3},
{"value": 1, "label": "sure", "probability": 0.7},
],
"confidence": 0.7,
},
]
_INPUT_TOKENS: Final = 367
_OUTPUT_TOKENS: Final = 3
_SYSTEM_ONE_USAGE: Final[dict[str, JsonValue]] = {"input_tokens": _INPUT_TOKENS, "output_tokens": _OUTPUT_TOKENS}
_USAGE: Final[dict[str, JsonValue]] = {
"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,
}
_SDK_QUESTIONS: Final = TypeAdapter(list[dict[str, object]]).validate_python(_QUESTIONS)
_SPEND_QUERY: Final = (
"SELECT spend, status, call_type, model_group, custom_llm_provider, api_base, prompt_tokens, completion_tokens, "
'request_tags FROM "LiteLLM_SpendLogs" WHERE request_id = %s'
)
@dataclass(frozen=True, slots=True)
class _Provider:
name: str
model: str
path: str
body_model: str
api_key: str | None
wraps_result: bool
cost_map_key: str | None
speaks_openai: bool = False
provider_reported_cost: float | None = None
def upstream_body(self) -> dict[str, JsonValue]:
if self.speaks_openai:
return {"model": self.body_model, "input": _INPUT, "questions": _QUESTIONS}
return {"model": self.body_model, "state": _INPUT, "questions": _SYSTEM_ONE_QUESTIONS}
def _with_reported_cost(self, usage: dict[str, JsonValue]) -> dict[str, JsonValue]:
if self.provider_reported_cost is None:
return usage
return {**usage, "cost": self.provider_reported_cost}
def upstream_reply(self) -> dict[str, JsonValue]:
if self.speaks_openai:
return {"model": self.body_model, "answers": _ANSWERS, "usage": self._with_reported_cost(_USAGE)}
answer: Final[dict[str, JsonValue]] = {
"model": self.body_model,
"answers": _SYSTEM_ONE_ANSWERS,
"usage": self._with_reported_cost(_SYSTEM_ONE_USAGE),
}
return {"result": answer, "success": True} if self.wraps_result else answer
def litellm_response(self) -> dict[str, JsonValue]:
return {"model": self.body_model, "answers": _ANSWERS, "usage": _USAGE}
_PROVIDERS: Final = (
_Provider(
"perplexity",
"perplexity/pplx-decider-v1-27b",
"/v1/decisions",
"pplx-decider-v1-27b",
_API_KEY,
False,
"perplexity/pplx-decider-v1-27b",
),
_Provider("typesafe", "typesafe/jev-1.13.0", "/v1/systemone", "jev-1.13.0", _API_KEY, False, "typesafe/jev-1.13.0"),
_Provider(
"openrouter",
"openrouter/typesafe/jev-1.13",
"/alpha/decisions",
"typesafe/jev-1.13",
_API_KEY,
False,
None,
provider_reported_cost=1.5834e-5,
),
_Provider(
"strands_decider", "strands_decider/systemone-decider", "/v1/systemone", "systemone-decider", None, False, None
),
_Provider(
"cloudflare",
"cloudflare/clef",
"/ai/run/@cf/cloudflare/clef",
"clef",
_API_KEY,
True,
"cloudflare/@cf/cloudflare/clef",
),
_Provider("hosted_vllm", "hosted_vllm/Qwen/Qwen3-0.6B", "/v1/systemone", "Qwen/Qwen3-0.6B", None, False, None),
_Provider(
"databricks",
"databricks/databricks-openjev-qwen35-4b",
"/databricks-openjev-qwen35-4b/invocations",
"databricks-openjev-qwen35-4b",
_API_KEY,
False,
None,
),
_Provider(
"azure_ai", "azure_ai/decision-1", "/providers/microsoft/v1/systemone", "decision-1", _API_KEY, False, None
),
_Provider("openai", "openai/gpt-6-luna", "/v1/decisions", "gpt-6-luna", _API_KEY, False, "gpt-6-luna", True),
)
_SYSTEM_ONE_PROVIDERS: Final = tuple(provider for provider in _PROVIDERS if not provider.speaks_openai)
_PERPLEXITY: Final = _PROVIDERS[0]
_TYPESAFE: Final = _PROVIDERS[1]
_OPENAI: Final = next(provider for provider in _PROVIDERS if provider.speaks_openai)
_PREDICATE: Final[dict[str, JsonValue]] = {"type": "predicate", "name": "q", "instructions": "Is it?"}
_INVALID_BODIES: Final[tuple[tuple[str, dict[str, JsonValue]], ...]] = (
("missing questions", {"input": _INPUT}),
("missing input", {"questions": _QUESTIONS}),
("numeric input", {"input": 5, "questions": _QUESTIONS}),
("assistant message input", {"input": [{"role": "assistant", "content": "hi"}], "questions": _QUESTIONS}),
("empty questions", {"input": _INPUT, "questions": []}),
("questions as a map", {"input": _INPUT, "questions": {"q": _PREDICATE}}),
("predicate without instructions", {"input": _INPUT, "questions": [{"type": "predicate", "name": "q"}]}),
(
"choice with one choice",
{"input": _INPUT, "questions": [{**_PREDICATE, "type": "choice", "choices": [{"value": "only"}]}]},
),
(
"score with one level",
{"input": _INPUT, "questions": [{**_PREDICATE, "type": "score", "levels": [{"label": "only"}]}]},
),
("unknown question type", {"input": _INPUT, "questions": [{**_PREDICATE, "type": "ranking"}]}),
("numeric safety_identifier", {"input": _INPUT, "questions": _QUESTIONS, "safety_identifier": 7}),
("list safety_identifier", {"input": _INPUT, "questions": _QUESTIONS, "safety_identifier": [_SAFETY_IDENTIFIER]}),
)
_IMAGE_INPUT: Final[list[JsonValue]] = [
{
"role": "user",
"content": [
{"type": "input_text", "text": _INPUT},
{"type": "input_image", "image_url": "data:image/png;base64,iVBORw0KGgo=", "detail": "auto"},
],
}
]
_OPENAI_IMAGE_MESSAGES: Final[list[JsonValue]] = [
{
"role": "user",
"type": "message",
"content": [
{"type": "input_text", "text": _INPUT},
{"type": "input_image", "image_url": "data:image/png;base64,iVBORw0KGgo=", "detail": "auto"},
],
}
]
_MESSAGE_LIST_INPUT: Final[list[JsonValue]] = [
{"role": "user", "content": _INPUT},
{"role": "user", "content": [{"type": "input_text", "text": _FOLLOW_UP}]},
]
def _response_cost(response: httpx.Response) -> float:
return float(response.headers["x-litellm-response-cost"]) if "x-litellm-response-cost" in response.headers else 0.0
def _provider_id(provider: _Provider) -> str:
return provider.name
def _number(value: JsonValue) -> float:
assert isinstance(value, (int, float)) and not isinstance(value, bool), value
return float(value)
def _expected_spend(provider: _Provider) -> float:
if provider.provider_reported_cost is not None:
return provider.provider_reported_cost
if provider.cost_map_key is None:
return 0.0
cost_map: Final = _JSON_OBJECT.validate_json(Path("model_prices_and_context_window.json").read_bytes())
prices: Final = object_value(cost_map[provider.cost_map_key])
return _INPUT_TOKENS * _number(prices["input_cost_per_token"]) + _OUTPUT_TOKENS * _number(
prices["output_cost_per_token"]
)
def _register(scenario: Scenario, body: dict[str, JsonValue], *, status: int = 200) -> ScenarioHandle:
handle: Final = register_scenario(
f"decisions-{uuid.uuid4().hex[:12]}", JsonResponse(content_type="application/json", body=body, status=status)
)
scenario.cleanups.callback(delete_scenario, handle)
return handle
def _deployment(scenario: Scenario, handle: ScenarioHandle, provider: _Provider, *, drop_params: bool = False) -> str:
if drop_params:
return scenario.model(
model=provider.model, api_base=handle.api_base(), api_key=provider.api_key, drop_params=True
)
return scenario.model(model=provider.model, api_base=handle.api_base(), api_key=provider.api_key)
def _decide(gateway: Gateway, model: str, *, route: str = "/v1/decisions", **extra: JsonValue) -> httpx.Response:
return gateway.request("POST", route, {"model": model, "input": _INPUT, "questions": _QUESTIONS, **extra})
def _decide_in_system_one_format(gateway: Gateway, model: str, **extra: JsonValue) -> httpx.Response:
return gateway.request(
"POST", "/v1/systemone", {"model": model, "state": _INPUT, "questions": _SYSTEM_ONE_QUESTIONS, **extra}
)
def _observed_requests(gateway: Gateway) -> tuple[dict[str, JsonValue], ...]:
with httpx.Client(base_url=gateway.upstream_url, timeout=5, trust_env=False) as upstream:
observations: Final = _JSON_OBJECT.validate_json(upstream.get("/__observations").content)["requests"]
assert isinstance(observations, list), observations
return tuple(map(object_value, observations))
def _calls_to(requests: Sequence[dict[str, JsonValue]], handle: ScenarioHandle) -> list[dict[str, JsonValue]]:
return [request for request in requests if string_value(request["path"]).startswith(f"/{handle.scenario_id}/")]
def _upstream_calls(gateway: Gateway, handle: ScenarioHandle) -> list[dict[str, JsonValue]]:
return _calls_to(_observed_requests(gateway), handle)
def _spend_row(call_id: str) -> dict[str, JsonValue]:
rows: Final = eventually(lambda: read_rows(_SPEND_QUERY, (call_id,)), lambda found: len(found) == 1, seconds=70)
return rows[0]
def _assert_refused_as_invalid(gateway: Gateway, model: str, label: str, body: dict[str, JsonValue]) -> None:
response: Final = gateway.request("POST", "/v1/decisions", {"model": model, **body})
assert response.status_code == 400, (label, response.text)
assert "Invalid Decisions request" in response.text, (label, response.text)
def _assert_refused_for_safety_identifier(response: httpx.Response) -> None:
assert response.status_code == 400, response.text
assert "safety_identifier" in response.text and "drop_params" in response.text, response.text
def _drop_params_config(directory: Path, api_base: str) -> Path:
base: Final = _JSON_OBJECT.validate_python(yaml.safe_load(Path("tests/integration/proxy_config.yaml").read_text()))
config: Final = {
**base,
"litellm_settings": {**object_value(base["litellm_settings"]), "drop_params": True},
"model_list": [
{
"model_name": _DROPPING_DEPLOYMENT,
"litellm_params": {"model": _PERPLEXITY.model, "api_base": api_base, "api_key": _API_KEY},
}
],
}
path: Final = directory / "decisions-drop-params.yaml"
path.write_text(yaml.safe_dump(config))
return path
@pytest.mark.parametrize("provider", _PROVIDERS, ids=_provider_id)
def test_each_provider_gets_its_own_path_key_and_body_and_is_billed(gateway: Gateway, provider: _Provider) -> None:
expected_spend: Final = _expected_spend(provider)
with gateway.scenario() as scenario:
handle: Final = _register(scenario, provider.upstream_reply())
model: Final = _deployment(scenario, handle, provider)
response: Final = _decide(gateway, model)
assert response.status_code == 200, response.text
assert response.json() == provider.litellm_response()
assert response.headers["x-litellm-model-group"] == model
assert math.isclose(_response_cost(response), expected_spend, rel_tol=1e-9)
(call,) = _upstream_calls(gateway, handle)
assert call["path"] == f"/{handle.scenario_id}{provider.path}"
assert call["authorization"] == (f"Bearer {provider.api_key}" if provider.api_key else "")
assert call["body"] == provider.upstream_body()
row: Final = _spend_row(response.headers["x-litellm-call-id"])
assert (
row["status"],
row["call_type"],
row["custom_llm_provider"],
row["model_group"],
row["api_base"],
row["prompt_tokens"],
row["completion_tokens"],
) == (
"success",
"adecisions",
provider.name,
model,
f"{handle.api_base()}{provider.path}",
_INPUT_TOKENS,
_OUTPUT_TOKENS,
)
assert math.isclose(_number(row["spend"]), expected_spend, rel_tol=1e-9), row
def test_the_unversioned_alias_serves_the_same_request(gateway: Gateway) -> None:
with gateway.scenario() as scenario:
handle: Final = _register(scenario, _PERPLEXITY.upstream_reply())
model: Final = _deployment(scenario, handle, _PERPLEXITY)
response: Final = _decide(gateway, model, route="/decisions")
assert response.status_code == 200, response.text
assert response.json() == _PERPLEXITY.litellm_response()
(call,) = _upstream_calls(gateway, handle)
assert call["body"] == _PERPLEXITY.upstream_body()
def test_repeated_identical_requests_each_reach_the_upstream_and_are_each_billed(gateway: Gateway) -> None:
with gateway.scenario() as scenario:
handle: Final = _register(scenario, _PERPLEXITY.upstream_reply())
model: Final = _deployment(scenario, handle, _PERPLEXITY)
responses: Final = tuple(_decide(gateway, model) for _ in range(2))
assert [response.status_code for response in responses] == [200, 200], [r.text for r in responses]
call_ids: Final = tuple(response.headers["x-litellm-call-id"] for response in responses)
assert len(set(call_ids)) == 2, call_ids
assert len(_upstream_calls(gateway, handle)) == 2
for call_id in call_ids:
assert _spend_row(call_id)["status"] == "success"
async def test_sdk_sync_and_async_clients_send_the_same_request(gateway: Gateway) -> None:
with gateway.scenario() as scenario:
handle: Final = _register(scenario, _TYPESAFE.upstream_reply())
synchronous: Final = litellm.decisions(
model=_TYPESAFE.model, input=_INPUT, questions=_SDK_QUESTIONS, api_base=handle.api_base(), api_key=_API_KEY
)
asynchronous: Final = await litellm.adecisions(
model=_TYPESAFE.model, input=_INPUT, questions=_SDK_QUESTIONS, api_base=handle.api_base(), api_key=_API_KEY
)
for response in (synchronous, asynchronous):
assert response.model_dump(mode="json") == _TYPESAFE.litellm_response()
calls: Final = _upstream_calls(gateway, handle)
assert len(calls) == 2, calls
for call in calls:
assert call["path"] == f"/{handle.scenario_id}{_TYPESAFE.path}"
assert call["authorization"] == f"Bearer {_API_KEY}"
assert call["body"] == _TYPESAFE.upstream_body()
def test_gateway_only_fields_stay_at_the_gateway_and_tags_reach_the_spend_log(gateway: Gateway) -> None:
tag: Final = f"decisions-openai-format-{uuid.uuid4().hex[:8]}"
with gateway.scenario() as scenario:
handle: Final = _register(scenario, _PERPLEXITY.upstream_reply())
model: Final = _deployment(scenario, handle, _PERPLEXITY)
response: Final = _decide(
gateway, model, user="auditor", num_retries=0, temperature=0.2, metadata={"tags": [tag]}
)
assert response.status_code == 200, response.text
(call,) = _upstream_calls(gateway, handle)
assert call["body"] == _PERPLEXITY.upstream_body()
row: Final = _spend_row(response.headers["x-litellm-call-id"])
tags: Final = row["request_tags"]
assert isinstance(tags, list) and tag in tags, row
def test_invalid_bodies_are_refused_at_the_gateway_without_an_upstream_call(gateway: Gateway) -> None:
with gateway.scenario() as scenario:
handle: Final = _register(scenario, _PERPLEXITY.upstream_reply())
model: Final = _deployment(scenario, handle, _PERPLEXITY)
for label, body in _INVALID_BODIES:
_assert_refused_as_invalid(gateway, model, label, body)
assert _upstream_calls(gateway, handle) == []
@pytest.mark.parametrize("provider", _SYSTEM_ONE_PROVIDERS, ids=_provider_id)
def test_system_one_providers_refuse_images_at_the_gateway_without_an_upstream_call(
gateway: Gateway, provider: _Provider
) -> None:
with gateway.scenario() as scenario:
handle: Final = _register(scenario, provider.upstream_reply())
model: Final = _deployment(scenario, handle, provider)
response: Final = _decide(gateway, model, input=_IMAGE_INPUT)
assert response.status_code == 400, response.text
assert "input_image" in response.text
assert _upstream_calls(gateway, handle) == []
def test_openai_forwards_image_input_in_its_own_message_shape(gateway: Gateway) -> None:
with gateway.scenario() as scenario:
handle: Final = _register(scenario, _OPENAI.upstream_reply())
model: Final = _deployment(scenario, handle, _OPENAI)
response: Final = _decide(gateway, model, input=_IMAGE_INPUT)
assert response.status_code == 200, response.text
(call,) = _upstream_calls(gateway, handle)
assert call["body"] == {"model": _OPENAI.body_model, "input": _OPENAI_IMAGE_MESSAGES, "questions": _QUESTIONS}
def test_a_message_list_input_reaches_a_system_one_provider_as_its_flattened_text(gateway: Gateway) -> None:
with gateway.scenario() as scenario:
handle: Final = _register(scenario, _PERPLEXITY.upstream_reply())
model: Final = _deployment(scenario, handle, _PERPLEXITY)
response: Final = _decide(gateway, model, input=_MESSAGE_LIST_INPUT)
assert response.status_code == 200, response.text
assert response.json() == _PERPLEXITY.litellm_response()
(call,) = _upstream_calls(gateway, handle)
assert call["body"] == {
"model": _PERPLEXITY.body_model,
"state": f"{_INPUT}\n\n{_FOLLOW_UP}",
"questions": _SYSTEM_ONE_QUESTIONS,
}
row: Final = _spend_row(response.headers["x-litellm-call-id"])
assert (row["status"], row["call_type"], row["prompt_tokens"], row["completion_tokens"]) == (
"success",
"adecisions",
_INPUT_TOKENS,
_OUTPUT_TOKENS,
)
@pytest.mark.parametrize("provider", _SYSTEM_ONE_PROVIDERS, ids=_provider_id)
def test_safety_identifier_is_refused_by_system_one_providers_unless_the_deployment_drops_params(
gateway: Gateway, provider: _Provider
) -> None:
with gateway.scenario() as scenario:
handle: Final = _register(scenario, provider.upstream_reply())
strict: Final = _deployment(scenario, handle, provider)
refused: Final = _decide(gateway, strict, safety_identifier=_SAFETY_IDENTIFIER)
_assert_refused_for_safety_identifier(refused)
assert _upstream_calls(gateway, handle) == []
dropping: Final = _deployment(scenario, handle, provider, drop_params=True)
accepted: Final = _decide(gateway, dropping, safety_identifier=_SAFETY_IDENTIFIER)
assert accepted.status_code == 200, accepted.text
assert accepted.json() == provider.litellm_response()
(call,) = _upstream_calls(gateway, handle)
assert call["body"] == provider.upstream_body()
row: Final = _spend_row(accepted.headers["x-litellm-call-id"])
assert (row["status"], row["call_type"], row["model_group"]) == ("success", "adecisions", dropping)
@pytest.mark.parametrize("value", ("", "x" * 5000), ids=("empty", "5kb"))
def test_every_string_safety_identifier_is_refused_or_dropped_like_the_usual_one(gateway: Gateway, value: str) -> None:
with gateway.scenario() as scenario:
handle: Final = _register(scenario, _PERPLEXITY.upstream_reply())
strict: Final = _deployment(scenario, handle, _PERPLEXITY)
_assert_refused_for_safety_identifier(_decide(gateway, strict, safety_identifier=value))
dropping: Final = _deployment(scenario, handle, _PERPLEXITY, drop_params=True)
accepted: Final = _decide(gateway, dropping, safety_identifier=value)
assert accepted.status_code == 200, accepted.text
(call,) = _upstream_calls(gateway, handle)
assert call["body"] == _PERPLEXITY.upstream_body()
def test_a_request_body_drop_params_drops_the_safety_identifier_like_chat(gateway: Gateway) -> None:
with gateway.scenario() as scenario:
handle: Final = _register(scenario, _PERPLEXITY.upstream_reply())
strict: Final = _deployment(scenario, handle, _PERPLEXITY)
response: Final = _decide(gateway, strict, safety_identifier=_SAFETY_IDENTIFIER, drop_params=True)
assert response.status_code == 200, response.text
assert response.json() == _PERPLEXITY.litellm_response()
(call,) = _upstream_calls(gateway, handle)
assert call["body"] == _PERPLEXITY.upstream_body()
def test_openai_keeps_the_safety_identifier_on_the_wire_without_drop_params(gateway: Gateway) -> None:
with gateway.scenario() as scenario:
handle: Final = _register(scenario, _OPENAI.upstream_reply())
model: Final = _deployment(scenario, handle, _OPENAI)
response: Final = _decide(gateway, model, safety_identifier=_SAFETY_IDENTIFIER)
assert response.status_code == 200, response.text
assert response.json() == _OPENAI.litellm_response()
(call,) = _upstream_calls(gateway, handle)
assert call["body"] == {**_OPENAI.upstream_body(), "safety_identifier": _SAFETY_IDENTIFIER}
def test_a_system_one_format_safety_identifier_is_refused_unless_the_deployment_drops_params(gateway: Gateway) -> None:
with gateway.scenario() as scenario:
handle: Final = _register(scenario, _PERPLEXITY.upstream_reply())
strict: Final = _deployment(scenario, handle, _PERPLEXITY)
refused: Final = _decide_in_system_one_format(gateway, strict, safety_identifier=_SAFETY_IDENTIFIER)
_assert_refused_for_safety_identifier(refused)
assert _upstream_calls(gateway, handle) == []
dropping: Final = _deployment(scenario, handle, _PERPLEXITY, drop_params=True)
accepted: Final = _decide_in_system_one_format(gateway, dropping, safety_identifier=_SAFETY_IDENTIFIER)
assert accepted.status_code == 200, accepted.text
assert accepted.json()["answers"] == _SYSTEM_ONE_ANSWERS
(call,) = _upstream_calls(gateway, handle)
assert call["body"] == _PERPLEXITY.upstream_body()
def test_openai_keeps_a_system_one_format_safety_identifier_on_the_wire(gateway: Gateway) -> None:
with gateway.scenario() as scenario:
handle: Final = _register(scenario, _OPENAI.upstream_reply())
model: Final = _deployment(scenario, handle, _OPENAI)
response: Final = _decide_in_system_one_format(gateway, model, safety_identifier=_SAFETY_IDENTIFIER)
assert response.status_code == 200, response.text
(call,) = _upstream_calls(gateway, handle)
assert call["body"] == {**_OPENAI.upstream_body(), "safety_identifier": _SAFETY_IDENTIFIER}
@pytest.mark.parametrize("decide", (_decide, _decide_in_system_one_format), ids=("openai_format", "system_one_format"))
@pytest.mark.parametrize("value", (7, [_SAFETY_IDENTIFIER]), ids=("numeric", "list"))
def test_a_non_string_safety_identifier_is_refused_as_invalid_unless_the_deployment_drops_params(
gateway: Gateway, value: JsonValue, decide: Callable[..., httpx.Response]
) -> None:
with gateway.scenario() as scenario:
handle: Final = _register(scenario, _OPENAI.upstream_reply())
strict: Final = _deployment(scenario, handle, _OPENAI)
refused: Final = decide(gateway, strict, safety_identifier=value)
assert refused.status_code == 400, refused.text
assert "Invalid Decisions request" in refused.text, refused.text
assert _upstream_calls(gateway, handle) == []
dropping: Final = _deployment(scenario, handle, _OPENAI, drop_params=True)
accepted: Final = decide(gateway, dropping, safety_identifier=value)
assert accepted.status_code == 200, accepted.text
(call,) = _upstream_calls(gateway, handle)
assert call["body"] == _OPENAI.upstream_body()
def test_litellm_settings_drop_params_drops_the_safety_identifier_for_a_strict_deployment(
gateway: Gateway, tmp_path: Path
) -> None:
with gateway.scenario() as scenario:
handle: Final = _register(scenario, _PERPLEXITY.upstream_reply())
config: Final = _drop_params_config(tmp_path, handle.api_base())
with owned_proxy_process(gateway, tmp_path, {}, config=config) as owned:
response: Final = _decide(owned.gateway, _DROPPING_DEPLOYMENT, safety_identifier=_SAFETY_IDENTIFIER)
assert response.status_code == 200, response.text
assert response.json() == _PERPLEXITY.litellm_response()
(call,) = _upstream_calls(gateway, handle)
assert call["body"] == _PERPLEXITY.upstream_body()

View file

@ -69,6 +69,7 @@ class _Provider:
api_key: str | None
wraps_result: bool
cost_map_key: str | None
provider_reported_cost: float | None = None
_PROVIDERS: Final = (
@ -89,11 +90,15 @@ _PROVIDERS: Final = (
"typesafe/jev-1.13",
_API_KEY,
False,
"openrouter/typesafe/jev-1.13",
None,
1.5834e-5,
),
_Provider(
"strands_decider", "strands_decider/systemone-decider", "/v1/systemone", "systemone-decider", None, False, None
),
_Provider(
"hosted_vllm", "hosted_vllm/Qwen/Qwen3-0.6B", "/v1/systemone", "Qwen/Qwen3-0.6B", None, False, None
),
_Provider(
"cloudflare",
"cloudflare/clef",
@ -106,6 +111,15 @@ _PROVIDERS: Final = (
_Provider(
"azure_ai", "azure_ai/decision-1", "/providers/microsoft/v1/systemone", "decision-1", _API_KEY, False, None
),
_Provider(
"databricks",
"databricks/databricks-openjev-qwen35-4b",
"/databricks-openjev-qwen35-4b/invocations",
"databricks-openjev-qwen35-4b",
_API_KEY,
False,
None,
),
)
_PERPLEXITY: Final = _PROVIDERS[0]
_OPENROUTER: Final = _PROVIDERS[2]
@ -133,17 +147,32 @@ def _number(value: JsonValue) -> float:
return float(value)
def _expected_spend(cost_map_key: str | None) -> float:
if cost_map_key is None:
def _expected_spend(provider: _Provider) -> float:
if provider.provider_reported_cost is not None:
return provider.provider_reported_cost
if provider.cost_map_key is None:
return 0.0
prices: Final = object_value(json.loads(Path("model_prices_and_context_window.json").read_text())[cost_map_key])
prices: Final = object_value(
json.loads(Path("model_prices_and_context_window.json").read_text())[provider.cost_map_key]
)
return _number(_USAGE["input_tokens"]) * _number(prices["input_cost_per_token"]) + _number(
_USAGE["output_tokens"]
) * _number(prices["output_cost_per_token"])
def _usage(provider: _Provider) -> dict[str, JsonValue]:
return {
**_USAGE,
**({"cost": provider.provider_reported_cost} if provider.provider_reported_cost is not None else {}),
}
def _answer_body(provider: _Provider) -> dict[str, JsonValue]:
answer: Final[dict[str, JsonValue]] = {"model": provider.body_model, "answers": _ANSWERS, "usage": _USAGE}
answer: Final[dict[str, JsonValue]] = {
"model": provider.body_model,
"answers": _ANSWERS,
"usage": _usage(provider),
}
return {"result": answer, "success": True} if provider.wraps_result else answer
@ -226,16 +255,14 @@ def _pass_through_config(directory: Path, pass_through_target: str, native_api_b
@pytest.mark.parametrize("provider", _PROVIDERS, ids=lambda provider: provider.name)
def test_each_provider_gets_its_own_path_key_and_body_and_is_billed_from_the_cost_map(
gateway: Gateway, provider: _Provider
) -> None:
expected_spend: Final = _expected_spend(provider.cost_map_key)
def test_each_provider_gets_its_own_path_key_and_body_and_is_billed(gateway: Gateway, provider: _Provider) -> None:
expected_spend: Final = _expected_spend(provider)
with gateway.scenario() as scenario:
handle: Final = _register(scenario, _answer_body(provider))
model: Final = _deployment(scenario, handle, provider)
response: Final = _decide(gateway, model)
assert response.status_code == 200, response.text
assert response.json() == {"model": provider.body_model, "answers": _ANSWERS, "usage": _USAGE}
assert response.json() == {"model": provider.body_model, "answers": _ANSWERS, "usage": _usage(provider)}
assert response.headers["x-litellm-model-group"] == model
assert math.isclose(float(response.headers.get("x-litellm-response-cost", "0")), expected_spend, rel_tol=1e-9)
(call,) = _upstream_calls(gateway, handle)
@ -251,7 +278,7 @@ def test_each_provider_gets_its_own_path_key_and_body_and_is_billed_from_the_cos
row["api_base"],
row["prompt_tokens"],
row["completion_tokens"],
) == ("success", "adecisions", provider.name, model, f"{handle.api_base()}{provider.path}", 367, 3)
) == ("success", "asystemone", provider.name, model, f"{handle.api_base()}{provider.path}", 367, 3)
assert math.isclose(_number(row["spend"]), expected_spend, rel_tol=1e-9), row
@ -292,7 +319,7 @@ async def test_sdk_sync_and_async_clients_send_the_same_request(gateway: Gateway
synchronous: Final = litellm.decisions(
model=provider.model, state=_STATE, questions=_SDK_QUESTIONS, api_base=handle.api_base(), api_key=_API_KEY
)
asynchronous: Final = await litellm.adecisions(
asynchronous: Final = await litellm.asystemone(
model=provider.model, state=_STATE, questions=_SDK_QUESTIONS, api_base=handle.api_base(), api_key=_API_KEY
)
for response in (synchronous, asynchronous):
@ -460,7 +487,7 @@ def test_upstream_errors_keep_their_status_and_log_an_unbilled_failure(gateway:
row: Final = _spend_row(response.headers["x-litellm-call-id"])
assert (row["status"], row["call_type"], row["model_group"], _number(row["spend"])) == (
"failure",
"adecisions",
"asystemone",
model,
0.0,
)
@ -532,7 +559,7 @@ async def test_sdk_openrouter_connection_failures_raise_a_connection_error(gatew
model=_OPENROUTER.model, state=_STATE, questions=_SDK_QUESTIONS, api_base=dead_api_base, api_key=_API_KEY
)
with pytest.raises(litellm.APIConnectionError):
await litellm.adecisions(
await litellm.asystemone(
model=_OPENROUTER.model, state=_STATE, questions=_SDK_QUESTIONS, api_base=dead_api_base, api_key=_API_KEY
)

View file

@ -0,0 +1,827 @@
import hashlib
import json
import os
import re
import threading
import time
import uuid
from collections.abc import Callable, Iterator, Mapping, Sequence
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass, replace
from functools import partial
from pathlib import Path
from typing import Final
from urllib.parse import quote, unquote
import httpx
import pytest
import yaml
from integration._support.client import JSON_OBJECT, Gateway, Scenario, eventually, gateway_from_environment
from integration._support.database import read_rows
from integration._support.forward_proxy import ForwardProxy, tunnelling_forward_proxy
from integration._support.process import owned_proxy
from integration._support.sigv4 import encoded_path, signature
from integration._support.tls import server_context, write_self_signed_cert
from integration._support.wire import Reply, Request, Wire, wire_server
from pydantic import JsonValue, TypeAdapter
import litellm
from litellm.llms.strands_decider.decisions.transformation import StrandsDeciderDecisionsConfig
_ACCOUNT: Final = "123456789012"
_ACCESS_KEY: Final = "AKIAAGENTCOREAUDIT01"
_SECRET_KEY: Final = "synthetic-agentcore-audit-secret"
_SIGNING: Final[Mapping[str, JsonValue]] = {"aws_access_key_id": _ACCESS_KEY, "aws_secret_access_key": _SECRET_KEY}
_JWT: Final = "synthetic-agentcore-jwt"
_ENV_JWT: Final = "synthetic-agentcore-env-jwt"
_US_EAST: Final = "us-east-1"
_EU_WEST: Final = "eu-west-1"
_MODEL: Final = "strands_decider/systemone-decider"
_BODY_MODEL: Final = "systemone-decider"
_SESSION_HEADER: Final = "x-amzn-bedrock-agentcore-runtime-session-id"
_CONFIGURED_SESSION: Final = "agentcore-audit-configured-session-0000000001"
_STATE: Final[dict[str, JsonValue]] = {"ticket": "Refund requested twice for one order", "queue": "payments"}
_QUESTIONS: Final[dict[str, JsonValue]] = {
"duplicate": {"type": "noul", "instructions": "Is this a duplicate refund request?"},
"urgency": {"type": "choice", "criteria": {"low": "can wait", "high": "money at risk"}},
}
_SDK_QUESTIONS: Final = TypeAdapter(dict[str, dict[str, object]]).validate_python(_QUESTIONS)
_ANSWERS: Final[dict[str, JsonValue]] = {
"duplicate": {"type": "noul", "noul": 0.88},
"urgency": {"type": "choice", "choice": "high", "confidence": 0.75, "probabilities": {"low": 0.25, "high": 0.75}},
}
_USAGE: Final[dict[str, JsonValue]] = {"input_tokens": 211, "output_tokens": 2}
_UPSTREAM_BODY: Final[dict[str, JsonValue]] = {"model": _BODY_MODEL, "state": _STATE, "questions": _QUESTIONS}
_INPUT: Final = "Refund requested twice for one order"
_OPENAI_QUESTIONS: Final[list[JsonValue]] = [
{"type": "predicate", "name": "duplicate", "instructions": "Is this a duplicate refund request?"},
{
"type": "choice",
"name": "urgency",
"instructions": "How urgent is it?",
"choices": [{"value": "low", "description": "can wait"}, {"value": "high", "description": "money at risk"}],
},
]
_OPENAI_UPSTREAM_BODY: Final[dict[str, JsonValue]] = {
"model": _BODY_MODEL,
"state": _INPUT,
"questions": {
"duplicate": {"type": "noul", "instructions": "Is this a duplicate refund request?"},
"urgency": {
"type": "choice",
"instructions": "How urgent is it?",
"criteria": {"low": "can wait", "high": "money at risk"},
},
},
}
_OPENAI_ANSWER: Final[dict[str, JsonValue]] = {
"model": _BODY_MODEL,
"answers": [
{"type": "predicate", "name": "duplicate", "probability": 0.88},
{
"type": "choice",
"name": "urgency",
"choice": "high",
"probabilities": [{"value": "low", "probability": 0.25}, {"value": "high", "probability": 0.75}],
"confidence": 0.75,
},
],
"usage": {
"input_tokens": 211,
"input_tokens_details": {"cached_tokens": 0, "cache_write_tokens": 0},
"output_tokens": 2,
"output_tokens_details": {"reasoning_tokens": 0},
"total_tokens": 213,
},
}
@dataclass(frozen=True, slots=True)
class _Route:
path: str
body: Mapping[str, JsonValue]
upstream: Mapping[str, JsonValue]
answer: Mapping[str, JsonValue]
_SYSTEMONE: Final = _Route(
"/v1/systemone",
{"state": _STATE, "questions": _QUESTIONS},
_UPSTREAM_BODY,
{"model": _BODY_MODEL, "answers": _ANSWERS, "usage": _USAGE},
)
_OPENAI: Final = _Route(
"/v1/decisions", {"input": _INPUT, "questions": _OPENAI_QUESTIONS}, _OPENAI_UPSTREAM_BODY, _OPENAI_ANSWER
)
_ROUTES: Final = (_SYSTEMONE, _OPENAI, replace(_SYSTEMONE, path="/systemone"), replace(_OPENAI, path="/decisions"))
_SPEND_QUERY: Final = (
"SELECT status, call_type, custom_llm_provider, model_group, api_base, prompt_tokens, completion_tokens "
'FROM "LiteLLM_SpendLogs" WHERE request_id = %s'
)
_RUNTIME_TARGET: Final = re.compile(
r"\A/runtimes/arn:aws:bedrock-agentcore:[a-z0-9-]+:[0-9]{12}:runtime/(?P<runtime>[A-Za-z0-9_-]+)/invocations\Z"
)
_ERROR_STATUS: Final = {"bad_request": 400, "invalid_request": 400, "loading": 503, "inference_error": 500}
_HTTP_ERRORS: Final = {
403: "AccessDeniedException",
404: "ResourceNotFoundException",
429: "ThrottlingException",
}
_MALFORMED_SESSIONS: Final[Mapping[str, JsonValue]] = {"int": 1234567, "list": ["a" * 40], "empty": ""}
_LONG_SESSION: Final = "s" * 5120
_NEAR_MISS_ARNS: Final = {
"endpoint": f"arn:aws:bedrock-agentcore:us-east-1:{_ACCOUNT}:runtime/audit_qualified/runtime-endpoint/DEFAULT",
"account": "arn:aws:bedrock-agentcore:us-east-1:12345:runtime/audit_short_account",
"service": f"arn:aws:bedrock:us-east-1:{_ACCOUNT}:runtime/audit_other_service",
"host": f"arn:aws:bedrock-agentcore:us-east-1.example.com:{_ACCOUNT}:runtime/audit_crafted_host",
}
_DEPLOYMENT_HEADERS: Final[dict[str, JsonValue]] = {
"x-amz-target": "AmazonBedrockAgentCore.Forged",
"X-Amzn-Bedrock-AgentCore-Runtime-Session-Id": "deployment-forged-session-00000000000001",
"Host": "bedrock-agentcore.us-west-2.amazonaws.com",
"x-audit-trace": "deployment-trace",
}
_SIGNED: Final = "agentcore-audit-signed"
_REGION: Final = "agentcore-audit-region"
_SESSION: Final = "agentcore-audit-session"
_LONG: Final = "agentcore-audit-session-long"
_BEARER: Final = "agentcore-audit-bearer"
_HEADERS: Final = "agentcore-audit-headers"
_OPT_IN: Final = "agentcore-audit-opt-in"
_HEALTH: Final = "agentcore-audit-health"
_DROPPED: Final = "agentcore-audit-dropped"
_OUTAGE: Final = "agentcore-audit-outage"
_ENV: Final = "agentcore-audit-env"
_UNSIGNED: Final = "agentcore-audit-unsigned"
_SCRUBBED_PREFIXES: Final = ("AWS_", "STRANDS_DECIDER_")
_SCRUBBED_NAMES: Final = frozenset(
{"HTTPS_PROXY", "HTTP_PROXY", "ALL_PROXY", "NO_PROXY", "SSL_VERIFY", "SSL_CERT_FILE", "REQUESTS_CA_BUNDLE"}
)
_load_yaml: Final[Callable[[str], object]] = yaml.safe_load
_JSON: Final[TypeAdapter[JsonValue]] = TypeAdapter(JsonValue)
_OWNED_PROXY_CELL_SECONDS: Final = 2 * max(30.0, float(os.environ.get("INTEGRATION_PROXY_READY_SECONDS") or 70)) + 120
pytestmark: Final = pytest.mark.timeout(_OWNED_PROXY_CELL_SECONDS)
def _host(region: str) -> str:
return f"bedrock-agentcore.{region}.amazonaws.com"
def _arn(runtime: str, region: str = _US_EAST) -> str:
return f"arn:aws:bedrock-agentcore:{region}:{_ACCOUNT}:runtime/{runtime}"
def _invocations_url(runtime: str, region: str = _US_EAST) -> str:
return f"https://{_host(region)}/runtimes/{quote(_arn(runtime, region), safe='')}/invocations"
def _default_session(runtime: str, region: str = _US_EAST) -> str:
return f"litellm-decider-{hashlib.sha256(_arn(runtime, region).encode()).hexdigest()}"
def _runtime_of(request: Request) -> str | None:
match: Final = _RUNTIME_TARGET.match(unquote(request.target))
return None if match is None else match["runtime"]
def _json_reply(body: Mapping[str, JsonValue], status: int = 200, **headers: str) -> Reply:
return Reply(status=status, body=json.dumps(body).encode(), headers=headers)
def _respond(outage: threading.Event, request: Request) -> Reply:
runtime: Final = _runtime_of(request) or ""
if runtime == "audit_dropped" or (runtime == "audit_outage" and outage.is_set()):
return Reply(drop_connection=True)
if runtime.startswith("audit_error_"):
code: Final = runtime.removeprefix("audit_error_")
return _json_reply({"error": {"code": code, "message": f"runtime refused with {code}"}})
if runtime.startswith("audit_status_"):
status: Final = int(runtime.removeprefix("audit_status_"))
return _json_reply(
{"message": f"scripted {_HTTP_ERRORS[status]}"}, status, **{"x-amzn-errortype": _HTTP_ERRORS[status]}
)
if not runtime:
return _json_reply({"message": f"unknown target {request.target}"}, 404)
return _json_reply({"model": _BODY_MODEL, "answers": _ANSWERS, "usage": _USAGE})
@dataclass(frozen=True, slots=True)
class _Runtime:
wire: Wire
tunnel: ForwardProxy
cert: Path
outage: threading.Event
def reset(self) -> None:
self.outage.clear()
self.wire.drain()
self.tunnel.drain()
def calls(self) -> tuple[Request, ...]:
return self.wire.drain()
def _calls_to(calls: Sequence[Request], runtime: str) -> tuple[Request, ...]:
return tuple(call for call in calls if _runtime_of(call) == runtime)
def _deployment(name: str, **litellm_params: JsonValue) -> dict[str, JsonValue]:
return {
"model_name": name,
"litellm_params": {"model": _MODEL, **litellm_params},
"model_info": {"id": name, "mode": "evaluation"},
}
def _signed_deployment(name: str, runtime: str, region: str = _US_EAST, **extra: JsonValue) -> dict[str, JsonValue]:
return _deployment(name, api_base=_arn(runtime, region), **_SIGNING, **extra)
def _deployments() -> list[dict[str, JsonValue]]:
return [
_signed_deployment(_SIGNED, "audit_signed", aws_region_name=_US_EAST),
_signed_deployment(_REGION, "audit_region", _EU_WEST, aws_region_name=_US_EAST),
_signed_deployment(_SESSION, "audit_session", agentcore_runtime_session_id=_CONFIGURED_SESSION),
_signed_deployment(_LONG, "audit_session_long", agentcore_runtime_session_id=_LONG_SESSION),
_deployment(_BEARER, api_base=_arn("audit_bearer"), api_key=_JWT),
_signed_deployment(_HEADERS, "audit_headers", extra_headers=_DEPLOYMENT_HEADERS),
_signed_deployment(
_OPT_IN, "audit_opt_in", configurable_clientside_auth_params=["agentcore_runtime_session_id"]
),
_signed_deployment(_HEALTH, "audit_health"),
_signed_deployment(_DROPPED, "audit_dropped"),
_signed_deployment(_OUTAGE, "audit_outage"),
_deployment(_ENV, **_SIGNING),
_deployment(_UNSIGNED, api_base=_arn("audit_unsigned")),
*(_signed_deployment(f"agentcore-audit-error-{code}", f"audit_error_{code}") for code in _ERROR_STATUS),
*(_signed_deployment(f"agentcore-audit-status-{status}", f"audit_status_{status}") for status in _HTTP_ERRORS),
*(
_signed_deployment(
f"agentcore-audit-session-{label}", f"audit_session_{label}", agentcore_runtime_session_id=value
)
for label, value in _MALFORMED_SESSIONS.items()
),
*(
_deployment(f"agentcore-audit-near-miss-{label}", api_base=value, **_SIGNING)
for label, value in _NEAR_MISS_ARNS.items()
),
]
def _write_config(directory: Path) -> Path:
base: Final = JSON_OBJECT.validate_python(_load_yaml(Path("tests/integration/proxy_config.yaml").read_text()))
configuration: Final = {
**base,
"model_list": _deployments(),
"router_settings": {"disable_cooldowns": True, "num_retries": 0},
}
path: Final = directory / "agentcore.yaml"
path.write_text(yaml.safe_dump(configuration))
return path
def _scrubbed() -> tuple[str, ...]:
return tuple(
name for name in os.environ if name.upper().startswith(_SCRUBBED_PREFIXES) or name.upper() in _SCRUBBED_NAMES
)
def _isolated_aws(directory: Path) -> dict[str, str]:
return {
"AWS_EC2_METADATA_DISABLED": "true",
"AWS_SHARED_CREDENTIALS_FILE": str(directory / "absent-credentials"),
"AWS_CONFIG_FILE": str(directory / "absent-config"),
}
def _proxy_environment(runtime: _Runtime, directory: Path) -> dict[str, str]:
return {
"HTTPS_PROXY": runtime.tunnel.url,
"NO_PROXY": "127.0.0.1,localhost",
"SSL_CERT_FILE": str(runtime.cert),
"STRANDS_DECIDER_API_BASE": _arn("audit_env"),
**_isolated_aws(directory),
}
@pytest.fixture(scope="module")
def module_gateway() -> Iterator[Gateway]:
with gateway_from_environment() as value:
yield value
@pytest.fixture(scope="module")
def runtime(tmp_path_factory: pytest.TempPathFactory) -> Iterator[_Runtime]:
cert, key = write_self_signed_cert(tmp_path_factory.mktemp("agentcore-cert"), (_host(_US_EAST), _host(_EU_WEST)))
outage: Final = threading.Event()
with wire_server(partial(_respond, outage), tls=server_context(cert, key)) as wire:
with tunnelling_forward_proxy(int(wire.url.rsplit(":", 1)[1])) as tunnel:
yield _Runtime(wire, tunnel, cert, outage)
@pytest.fixture(scope="module")
def agentcore(
module_gateway: Gateway, runtime: _Runtime, tmp_path_factory: pytest.TempPathFactory
) -> Iterator[Gateway]:
directory: Final = tmp_path_factory.mktemp("agentcore-proxy")
with owned_proxy(
module_gateway,
directory,
_proxy_environment(runtime, directory),
config=_write_config(directory),
remove_environment=_scrubbed(),
workers=2,
) as candidate:
yield candidate
def _decide(gateway: Gateway, model: str, route: _Route = _SYSTEMONE, **extra: JsonValue) -> httpx.Response:
return gateway.request("POST", route.path, {"model": model, **route.body, **extra})
def _authorization_fields(request: Request) -> dict[str, str]:
authorization: Final = request.headers.get("authorization", "")
assert authorization.startswith("AWS4-HMAC-SHA256 "), dict(request.headers)
return dict(part.split("=", 1) for part in authorization.removeprefix("AWS4-HMAC-SHA256 ").split(", "))
def _assert_signed(request: Request, region: str = _US_EAST) -> None:
fields: Final = _authorization_fields(request)
access_key, scope = fields["Credential"].split("/", 1)
assert access_key == _ACCESS_KEY, fields
assert scope == f"{request.headers['x-amz-date'][:8]}/{region}/bedrock-agentcore/aws4_request", fields
signed: Final = fields["SignedHeaders"]
assert {"host", "x-amz-date", _SESSION_HEADER}.issubset(signed.split(";")), signed
expected: Final = signature(
"POST", encoded_path(request.target), request.headers, signed, request.body, _SECRET_KEY, scope
)
assert fields["Signature"] == expected[1], (request.target, fields)
def _assert_answered(response: httpx.Response, model: str, route: _Route = _SYSTEMONE) -> None:
assert response.status_code == 200, response.text
assert response.json() == route.answer, response.text
assert response.headers["x-litellm-model-group"] == model, dict(response.headers)
def _assert_runtime_target(request: Request, runtime: str, region: str = _US_EAST) -> None:
assert request.method == "POST", request.method
assert request.target == f"/runtimes/{quote(_arn(runtime, region), safe='')}/invocations", request.target
assert request.headers["host"] == _host(region), dict(request.headers)
def _assert_runtime_call(request: Request, runtime: str, region: str = _US_EAST, route: _Route = _SYSTEMONE) -> None:
_assert_runtime_target(request, runtime, region)
assert json.loads(request.body) == route.upstream, request.body
def _canonical(body: JsonValue | Mapping[str, JsonValue]) -> str:
return json.dumps(body, sort_keys=True)
def _spend_row(call_id: str) -> dict[str, JsonValue]:
rows: Final = eventually(lambda: read_rows(_SPEND_QUERY, (call_id,)), lambda found: len(found) == 1, seconds=70)
return rows[0]
def _spend_rows(call_ids: Sequence[str]) -> list[dict[str, JsonValue]]:
query: Final = (
"SELECT request_id, status FROM \"LiteLLM_SpendLogs\" WHERE request_id = ANY(string_to_array(%s, ','))"
)
return eventually(
lambda: read_rows(query, (",".join(call_ids),)), lambda found: len(found) >= len(call_ids), seconds=70
)
def _connect_targets(runtime: _Runtime) -> frozenset[str]:
return frozenset(runtime.tunnel.targets())
def test_an_arn_deployment_answers_on_every_decisions_route_signed_for_bedrock_agentcore(
agentcore: Gateway, runtime: _Runtime
) -> None:
runtime.reset()
responses: Final = tuple(_decide(agentcore, _SIGNED, route) for route in _ROUTES)
for response, route in zip(responses, _ROUTES, strict=True):
_assert_answered(response, _SIGNED, route)
assert _connect_targets(runtime) == {f"{_host(_US_EAST)}:443"}
calls: Final = runtime.calls()
assert len(calls) == len(_ROUTES), calls
for call, route in zip(calls, _ROUTES, strict=True):
_assert_runtime_call(call, "audit_signed", route=route)
assert call.headers[_SESSION_HEADER] == _default_session("audit_signed"), dict(call.headers)
_assert_signed(call)
for response in responses:
row = _spend_row(response.headers["x-litellm-call-id"])
assert row == {
"status": "success",
"call_type": "asystemone",
"custom_llm_provider": "strands_decider",
"model_group": _SIGNED,
"api_base": _invocations_url("audit_signed"),
"prompt_tokens": 211,
"completion_tokens": 2,
}, row
def test_the_arn_region_picks_the_host_and_signing_scope_over_aws_region_name(
agentcore: Gateway, runtime: _Runtime
) -> None:
runtime.reset()
_assert_answered(_decide(agentcore, _REGION), _REGION)
assert _connect_targets(runtime) == {f"{_host(_EU_WEST)}:443"}
(call,) = runtime.calls()
_assert_runtime_call(call, "audit_region", _EU_WEST)
_assert_signed(call, _EU_WEST)
def test_a_configured_session_id_is_sent_as_written_and_signed(agentcore: Gateway, runtime: _Runtime) -> None:
runtime.reset()
_assert_answered(_decide(agentcore, _SESSION), _SESSION)
_assert_answered(_decide(agentcore, _LONG), _LONG)
calls: Final = runtime.calls()
(configured,) = _calls_to(calls, "audit_session")
(long,) = _calls_to(calls, "audit_session_long")
assert configured.headers[_SESSION_HEADER] == _CONFIGURED_SESSION, dict(configured.headers)
assert long.headers[_SESSION_HEADER] == _LONG_SESSION
_assert_signed(configured)
_assert_signed(long)
def test_a_deployment_api_key_sends_bearer_and_the_session_without_sigv4(agentcore: Gateway, runtime: _Runtime) -> None:
runtime.reset()
_assert_answered(_decide(agentcore, _BEARER), _BEARER)
(call,) = runtime.calls()
_assert_runtime_call(call, "audit_bearer")
assert call.headers["authorization"] == f"Bearer {_JWT}", dict(call.headers)
assert call.headers[_SESSION_HEADER] == _default_session("audit_bearer"), dict(call.headers)
assert "x-amz-date" not in call.headers, dict(call.headers)
def test_reserved_extra_headers_are_dropped_and_the_rest_are_kept(agentcore: Gateway, runtime: _Runtime) -> None:
runtime.reset()
_assert_answered(_decide(agentcore, _HEADERS), _HEADERS)
caller_headers: Final[dict[str, JsonValue]] = {
"x-amz-security-token": "caller-forged-token",
"X-Amzn-Bedrock-AgentCore-Runtime-Session-Id": "caller-forged-session-0000000000000000001",
"x-audit-caller": "caller-trace",
}
_assert_answered(_decide(agentcore, _HEADERS, extra_headers=caller_headers), _HEADERS)
deployment, caller = runtime.calls()
for call in (deployment, caller):
_assert_runtime_call(call, "audit_headers")
assert call.headers[_SESSION_HEADER] == _default_session("audit_headers"), dict(call.headers)
assert "x-amz-target" not in call.headers, dict(call.headers)
assert "x-amz-security-token" not in call.headers, dict(call.headers)
_assert_signed(call)
assert deployment.headers["x-audit-trace"] == "deployment-trace", dict(deployment.headers)
assert caller.headers["x-audit-caller"] == "caller-trace", dict(caller.headers)
def test_an_opted_in_caller_session_id_becomes_the_header_and_stays_out_of_the_body(
agentcore: Gateway, runtime: _Runtime
) -> None:
runtime.reset()
chosen: Final = f"agentcore-audit-caller-session-{uuid.uuid4().hex}"
_assert_answered(_decide(agentcore, _OPT_IN, agentcore_runtime_session_id=chosen), _OPT_IN)
_assert_answered(_decide(agentcore, _OPT_IN), _OPT_IN)
first, second = runtime.calls()
for call in (first, second):
_assert_runtime_call(call, "audit_opt_in")
_assert_signed(call)
assert first.headers[_SESSION_HEADER] == chosen, dict(first.headers)
assert second.headers[_SESSION_HEADER] == _default_session("audit_opt_in"), dict(second.headers)
def test_an_arn_deployment_without_api_base_takes_the_runtime_from_the_environment(
agentcore: Gateway, runtime: _Runtime
) -> None:
runtime.reset()
response: Final = _decide(agentcore, _ENV)
_assert_answered(response, _ENV)
(call,) = runtime.calls()
_assert_runtime_call(call, "audit_env")
_assert_signed(call)
assert _spend_row(response.headers["x-litellm-call-id"])["api_base"] == _invocations_url("audit_env")
def test_an_arn_deployment_added_through_the_management_api_answers_signed(
agentcore: Gateway, runtime: _Runtime
) -> None:
runtime.reset()
with agentcore.scenario() as scenario:
model: Final = scenario.model(
model_info=None,
model=_MODEL,
api_base=_arn("audit_stored"),
api_key=None,
aws_access_key_id=_ACCESS_KEY,
aws_secret_access_key=_SECRET_KEY,
)
_assert_answered(_decide(agentcore, model), model)
(call,) = runtime.calls()
_assert_runtime_call(call, "audit_stored")
_assert_signed(call)
def test_health_and_test_connection_probe_the_runtime_signed(agentcore: Gateway, runtime: _Runtime) -> None:
runtime.reset()
health: Final = agentcore.request("GET", "/health", params={"model": _HEALTH})
assert health.status_code == 200, health.text
assert (health.json()["healthy_count"], health.json()["unhealthy_count"]) == (1, 0), health.text
(probe,) = runtime.calls()
assert probe.target == f"/runtimes/{quote(_arn('audit_health'), safe='')}/invocations", probe.target
_assert_signed(probe)
connection: Final = agentcore.request(
"POST",
"/health/test_connection",
{
"litellm_params": {"model": _MODEL, "api_base": _arn("audit_test_connection"), **_SIGNING},
"mode": "evaluation",
},
)
assert connection.status_code == 200, connection.text
assert connection.json()["status"] == "success", connection.text
(tested,) = runtime.calls()
assert _runtime_of(tested) == "audit_test_connection", tested.target
_assert_signed(tested)
@pytest.mark.parametrize(("code", "status"), tuple(_ERROR_STATUS.items()), ids=tuple(_ERROR_STATUS))
def test_a_runtime_error_reply_maps_to_its_status_with_the_runtime_message(
agentcore: Gateway, runtime: _Runtime, code: str, status: int
) -> None:
runtime.reset()
model: Final = f"agentcore-audit-error-{code}"
response: Final = _decide(agentcore, model)
assert response.status_code == status, response.text
assert f"Strands Decider runtime error '{code}': runtime refused with {code}" in response.text, response.text
assert len(_calls_to(runtime.calls(), f"audit_error_{code}")) == 1
row: Final = _spend_row(response.headers["x-litellm-call-id"])
assert (row["status"], row["model_group"]) == ("failure", model), row
@pytest.mark.parametrize("status", tuple(_HTTP_ERRORS), ids=tuple(str(status) for status in _HTTP_ERRORS))
def test_a_runtime_http_error_keeps_its_status_and_message(agentcore: Gateway, runtime: _Runtime, status: int) -> None:
runtime.reset()
model: Final = f"agentcore-audit-status-{status}"
response: Final = _decide(agentcore, model)
assert response.status_code == status, response.text
assert f"scripted {_HTTP_ERRORS[status]}" in response.text, response.text
assert len(_calls_to(runtime.calls(), f"audit_status_{status}")) == 1
assert _spend_row(response.headers["x-litellm-call-id"])["status"] == "failure"
@pytest.mark.parametrize("label", tuple(_MALFORMED_SESSIONS))
def test_a_malformed_configured_session_id_fails_only_its_own_deployment(
agentcore: Gateway, runtime: _Runtime, label: str
) -> None:
runtime.reset()
model: Final = f"agentcore-audit-session-{label}"
response: Final = _decide(agentcore, model)
calls: Final = _calls_to(runtime.calls(), f"audit_session_{label}")
if label == "empty":
_assert_answered(response, model)
(call,) = calls
assert call.headers[_SESSION_HEADER] == "", dict(call.headers)
else:
assert response.status_code == 500, response.text
assert "agentcore_runtime_session_id" in response.text, response.text
assert calls == ()
_assert_answered(_decide(agentcore, _SIGNED), _SIGNED)
def test_a_deployment_without_aws_credentials_fails_before_dialing_the_runtime(
agentcore: Gateway, runtime: _Runtime
) -> None:
runtime.reset()
response: Final = _decide(agentcore, _UNSIGNED)
assert response.status_code == 500, response.text
assert "credentials" in response.text.lower(), response.text
assert runtime.calls() == ()
assert _connect_targets(runtime) == frozenset()
_assert_answered(_decide(agentcore, _SIGNED), _SIGNED)
@pytest.mark.parametrize("label", tuple(_NEAR_MISS_ARNS))
def test_a_near_miss_arn_keeps_the_plain_api_base_path(agentcore: Gateway, runtime: _Runtime, label: str) -> None:
runtime.reset()
response: Final = _decide(agentcore, f"agentcore-audit-near-miss-{label}")
assert 500 <= response.status_code < 600, response.text
assert "litellm.APIConnectionError" in response.text, response.text
assert _connect_targets(runtime) == frozenset()
assert runtime.calls() == ()
def test_a_dropped_connection_is_a_connection_error_after_one_attempt(agentcore: Gateway, runtime: _Runtime) -> None:
runtime.reset()
response: Final = _decide(agentcore, _DROPPED)
assert response.status_code == 500, response.text
assert "litellm.APIConnectionError" in response.text, response.text
assert len(_calls_to(runtime.calls(), "audit_dropped")) == 1
assert _spend_row(response.headers["x-litellm-call-id"])["status"] == "failure"
def _burst_routes(size: int) -> tuple[_Route, ...]:
return tuple(_ROUTES[index % len(_ROUTES)] for index in range(size))
def _burst(gateway: Gateway, model: str, size: int) -> tuple[httpx.Response, ...]:
with ThreadPoolExecutor(max_workers=size) as pool:
return tuple(pool.map(partial(_decide, gateway, model), _burst_routes(size)))
def _health_counts(gateway: Gateway, model: str) -> tuple[int, int, int]:
response: Final = gateway.request("GET", "/health", params={"model": model})
body: Final = JSON_OBJECT.validate_json(response.content)
healthy: Final = body["healthy_count"]
unhealthy: Final = body["unhealthy_count"]
assert isinstance(healthy, int) and isinstance(unhealthy, int), body
return response.status_code, healthy, unhealthy
def test_a_runtime_outage_mid_burst_fails_each_call_once_and_recovers_signed(
agentcore: Gateway, runtime: _Runtime
) -> None:
runtime.reset()
runtime.outage.set()
failed: Final = _burst(agentcore, _OUTAGE, 10)
assert [response.status_code for response in failed] == [500] * 10, [response.text for response in failed]
assert _health_counts(agentcore, _OUTAGE) == (503, 0, 1)
runtime.outage.clear()
runtime.calls()
served: Final = _burst(agentcore, _OUTAGE, 20)
for response, route in zip(served, _burst_routes(20), strict=True):
_assert_answered(response, _OUTAGE, route)
calls: Final = runtime.calls()
assert len(calls) == 20, calls
for call in calls:
_assert_runtime_target(call, "audit_outage")
_assert_signed(call)
assert sorted(_canonical(_JSON.validate_json(call.body)) for call in calls) == sorted(
_canonical(route.upstream) for route in _burst_routes(20)
)
assert _health_counts(agentcore, _OUTAGE) == (200, 1, 0)
expected: Final = {
**{response.headers["x-litellm-call-id"]: "failure" for response in failed},
**{response.headers["x-litellm-call-id"]: "success" for response in served},
}
assert len(expected) == 30, expected
rows: Final = _spend_rows(tuple(expected))
assert sorted((str(row["request_id"]), row["status"]) for row in rows) == sorted(expected.items()), rows
async def test_sdk_sync_and_async_calls_reach_the_runtime_signed(
runtime: _Runtime, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
runtime.reset()
for name in _scrubbed():
monkeypatch.delenv(name)
for name, value in {
"HTTPS_PROXY": runtime.tunnel.url,
"NO_PROXY": "127.0.0.1,localhost",
"SSL_CERT_FILE": str(runtime.cert),
**_isolated_aws(tmp_path),
}.items():
monkeypatch.setenv(name, value)
litellm.in_memory_llm_clients_cache.flush_cache()
try:
synchronous: Final = litellm.decisions(
model=_MODEL,
state=_STATE,
questions=_SDK_QUESTIONS,
api_base=_arn("audit_sdk"),
aws_access_key_id=_ACCESS_KEY,
aws_secret_access_key=_SECRET_KEY,
)
asynchronous: Final = await litellm.asystemone(
model=_MODEL,
state=_STATE,
questions=_SDK_QUESTIONS,
api_base=_arn("audit_sdk"),
aws_access_key_id=_ACCESS_KEY,
aws_secret_access_key=_SECRET_KEY,
)
monkeypatch.setenv("STRANDS_DECIDER_API_KEY", _ENV_JWT)
bearer: Final = await litellm.asystemone(
model=_MODEL, state=_STATE, questions=_SDK_QUESTIONS, api_base=_arn("audit_sdk_env_key")
)
finally:
litellm.in_memory_llm_clients_cache.flush_cache()
for response in (synchronous, asynchronous, bearer):
assert response.model_dump(mode="json") == {"model": _BODY_MODEL, "answers": _ANSWERS, "usage": _USAGE}
calls: Final = runtime.calls()
signed: Final = _calls_to(calls, "audit_sdk")
assert len(signed) == 2, calls
for call in signed:
_assert_runtime_call(call, "audit_sdk")
assert call.headers[_SESSION_HEADER] == _default_session("audit_sdk"), dict(call.headers)
_assert_signed(call)
(env_key,) = _calls_to(calls, "audit_sdk_env_key")
assert env_key.headers["authorization"] == f"Bearer {_ENV_JWT}", dict(env_key.headers)
assert "x-amz-date" not in env_key.headers, dict(env_key.headers)
def _answering(request: Request) -> Reply:
return _json_reply({"model": _BODY_MODEL, "answers": _ANSWERS, "usage": _USAGE})
def _envelope(request: Request) -> Reply:
return _json_reply({"error": {"code": "bad_request", "message": "plain server refused"}})
def _plain_model(scenario: Scenario, wire: Wire, **extra: JsonValue) -> str:
return scenario.model(model_info=None, model=_MODEL, api_base=wire.url, api_key=None, **extra)
def test_a_request_body_session_id_is_refused_on_every_route_without_an_upstream_call(gateway: Gateway) -> None:
with wire_server(_answering) as wire, gateway.scenario() as scenario:
model: Final = _plain_model(scenario, wire)
responses: Final = tuple(
_decide(gateway, model, route, agentcore_runtime_session_id=f"caller-{uuid.uuid4().hex}")
for route in _ROUTES
)
for response in responses:
assert response.status_code == 401, response.text
assert "agentcore_runtime_session_id is not allowed in request body" in response.text, response.text
assert wire.drain() == ()
def test_a_plain_api_base_opted_into_the_session_param_sends_no_session_header_or_body_field(
gateway: Gateway,
) -> None:
with wire_server(_answering) as wire, gateway.scenario() as scenario:
model: Final = _plain_model(
scenario, wire, configurable_clientside_auth_params=["agentcore_runtime_session_id"]
)
_assert_answered(_decide(gateway, model, agentcore_runtime_session_id=f"caller-{uuid.uuid4().hex}"), model)
(call,) = wire.drain()
assert call.target == "/v1/systemone", call.target
assert _SESSION_HEADER not in call.headers, dict(call.headers)
assert json.loads(call.body) == _UPSTREAM_BODY, call.body
def test_a_plain_api_base_error_envelope_keeps_the_unexpected_response_server_error(gateway: Gateway) -> None:
with wire_server(_envelope) as wire, gateway.scenario() as scenario:
model: Final = _plain_model(scenario, wire)
response: Final = _decide(gateway, model)
assert response.status_code == 500, response.text
assert "Strands Decider runtime error" not in response.text, response.text
assert len(wire.drain()) == 1
def test_a_plain_api_base_forwards_amazon_shaped_extra_headers_unchanged(gateway: Gateway) -> None:
headers: Final[dict[str, JsonValue]] = {
"x-amz-target": "plain-target",
"X-Amzn-Bedrock-AgentCore-Runtime-Session-Id": "plain-session-kept",
"x-audit-trace": "plain-trace",
}
with wire_server(_answering) as wire, gateway.scenario() as scenario:
model: Final = _plain_model(scenario, wire, extra_headers=headers)
_assert_answered(_decide(gateway, model), model)
(call,) = wire.drain()
assert (call.headers["x-amz-target"], call.headers[_SESSION_HEADER], call.headers["x-audit-trace"]) == (
"plain-target",
"plain-session-kept",
"plain-trace",
), dict(call.headers)
assert "authorization" not in call.headers, dict(call.headers)
assert json.loads(call.body) == _UPSTREAM_BODY, call.body
def _worst_case_api_base() -> str:
return "arn:aws" + "-a" * 131072 + "!"
def test_a_worst_case_runtime_arn_pattern_input_is_classified_in_linear_time(
gateway: Gateway, record_property: Callable[[str, object], None]
) -> None:
worst: Final = _worst_case_api_base()
started: Final = time.perf_counter()
url: Final = StrandsDeciderDecisionsConfig().get_complete_url(worst, _BODY_MODEL)
classified: Final = time.perf_counter() - started
record_property("classifier_seconds", f"{classified:.6f}")
assert url == f"{worst}/v1/systemone"
assert classified < 0.5, classified
with wire_server(_answering) as wire, gateway.scenario() as scenario:
model: Final = _plain_model(scenario, wire, configurable_clientside_auth_params=["api_base"])
with ThreadPoolExecutor(max_workers=2) as pool:
timed: Final = pool.submit(_timed_decide, gateway, model, worst)
liveliness_started: Final = time.perf_counter()
liveliness: Final = gateway.request("GET", "/health/liveliness")
liveliness_seconds: Final = time.perf_counter() - liveliness_started
response, decide_seconds = timed.result()
record_property("decide_seconds", f"{decide_seconds:.6f}")
record_property("liveliness_seconds", f"{liveliness_seconds:.6f}")
assert liveliness.status_code == 200, liveliness.text
assert 500 <= response.status_code < 600, response.text
assert "litellm.APIConnectionError" in response.text, response.text[:2000]
assert decide_seconds < 5, decide_seconds
assert liveliness_seconds < 2, liveliness_seconds
assert wire.drain() == ()
def _timed_decide(gateway: Gateway, model: str, api_base: str) -> tuple[httpx.Response, float]:
started: Final = time.perf_counter()
response: Final = _decide(gateway, model, api_base=api_base)
return response, time.perf_counter() - started

View file

@ -346,6 +346,10 @@ model_list:
litellm_params:
model: strands_decider/strands-decider-2B-hobson-v19
api_base: http://127.0.0.1:8191
- model_name: hosted_vllm/Qwen/Qwen3-0.6B
litellm_params:
model: hosted_vllm/Qwen/Qwen3-0.6B
api_base: http://127.0.0.1:8191
- model_name: azure_ai/decision-1
litellm_params:
model: azure_ai/decision-1

File diff suppressed because it is too large Load diff

View file

@ -7,7 +7,8 @@ from pydantic import JsonValue
@dataclass(frozen=True, slots=True, kw_only=True)
class TranslationTestCase:
"""One request through the proxy to a deployment in `proxy_config.yaml`: what the test sends to LiteLLM, the
exact request the provider must receive, the fake provider's reply, and the exact response LiteLLM must return."""
exact request the provider must receive, the fake provider's status and reply, and the exact response LiteLLM must
return."""
scenario: str
litellm_endpoint: str
@ -15,6 +16,7 @@ class TranslationTestCase:
expected_provider_endpoint: str
expected_provider_headers: Mapping[str, str]
expected_provider_request: Mapping[str, JsonValue]
mock_provider_status_code: int = 200
mock_provider_response: Mapping[str, JsonValue]
expected_litellm_status_code: int = 200
expected_litellm_response: Mapping[str, JsonValue]

View file

@ -6,6 +6,103 @@ from integration.translation.case import TranslationTestCase
"""
CLEF_TEST_CASE: Final = TranslationTestCase(
scenario="basic",
litellm_endpoint="/v1/decisions",
litellm_request={
"model": "cloudflare/@cf/cloudflare/clef",
"input": "Ticket (billing): The export job hangs at 99% and never finishes",
"questions": [
{"type": "predicate", "name": "defect", "instructions": "Is this a defect?"},
{
"type": "choice",
"name": "severity",
"instructions": "How severe is it?",
"choices": [
{"value": "low", "description": "cosmetic"},
{"value": "high", "description": "blocks users"},
],
},
{
"type": "score",
"name": "confidence",
"instructions": "How sure are you?",
"levels": [{"label": "unsure"}, {"label": "sure"}],
},
],
"cache": {"no-cache": True},
},
expected_provider_endpoint="/ai/run/@cf/cloudflare/clef",
expected_provider_headers={"authorization": "Bearer synthetic-cloudflare-key", "content-type": "application/json"},
expected_provider_request={
"model": "clef",
"state": "Ticket (billing): The export job hangs at 99% and never finishes",
"questions": {
"defect": {"type": "noul", "instructions": "Is this a defect?"},
"severity": {
"type": "choice",
"instructions": "How severe is it?",
"criteria": {"low": "cosmetic", "high": "blocks users"},
},
"confidence": {"type": "score", "instructions": "How sure are you?", "criteria": ["unsure", "sure"]},
},
},
mock_provider_response={
"result": {
"model": "clef",
"answers": {
"defect": {"type": "noul", "noul": 0.9345},
"severity": {
"type": "choice",
"choice": "high",
"confidence": 0.8067,
"probabilities": {"low": 0.0509, "high": 0.9491},
},
"confidence": {
"type": "score",
"score": 0.9036,
"confidence": 0.6515,
"legend": {"0": "unsure", "1": "sure"},
"probabilities": {"0": 0.0964, "1": 0.9036},
},
},
"usage": {"input_tokens": 290, "output_tokens": 0},
},
"success": True,
"errors": [],
"messages": [],
},
expected_litellm_response={
"model": "clef",
"answers": [
{"type": "predicate", "name": "defect", "probability": 0.9345},
{
"type": "choice",
"name": "severity",
"choice": "high",
"probabilities": [{"value": "low", "probability": 0.0509}, {"value": "high", "probability": 0.9491}],
"confidence": 0.8067,
},
{
"type": "score",
"name": "confidence",
"score": 0.9036,
"probabilities": [
{"value": 0, "label": "unsure", "probability": 0.0964},
{"value": 1, "label": "sure", "probability": 0.9036},
],
"confidence": 0.6515,
},
],
"usage": {
"input_tokens": 290,
"input_tokens_details": {"cached_tokens": 0, "cache_write_tokens": 0},
"output_tokens": 0,
"output_tokens_details": {"reasoning_tokens": 0},
"total_tokens": 290,
},
},
)
CLEF_SYSTEMONE_TEST_CASE: Final = TranslationTestCase(
scenario="systemone",
litellm_endpoint="/v1/systemone",
litellm_request={
"model": "cloudflare/@cf/cloudflare/clef",

View file

@ -0,0 +1,175 @@
import json
from dataclasses import replace
from typing import Final
from integration.translation.case import TranslationTestCase
"""Provider request and reply shape from a real vLLM serve (POST /v1/systemone, no auth). Mock reply captured live on 2026-10-09 from a vLLM CPU build of vllm main 702ce313f serving Qwen/Qwen3-0.6B.
"""
QWEN3_0_6B_TEST_CASE: Final = TranslationTestCase(
scenario="basic",
litellm_endpoint="/v1/systemone",
litellm_request={
"model": "hosted_vllm/Qwen/Qwen3-0.6B",
"state": "Help! My payouts have been failing for 3 days!",
"questions": {
"is_urgent": {
"type": "choice",
"instructions": "Does this convey urgency?",
"criteria": {"yes": "the message conveys urgency", "no": "the message does not convey urgency"},
}
},
"cache": {"no-cache": True},
},
expected_provider_endpoint="/v1/systemone",
expected_provider_headers={"content-type": "application/json"},
expected_provider_request={
"model": "Qwen/Qwen3-0.6B",
"state": "Help! My payouts have been failing for 3 days!",
"questions": {
"is_urgent": {
"type": "choice",
"instructions": "Does this convey urgency?",
"criteria": {"yes": "the message conveys urgency", "no": "the message does not convey urgency"},
}
},
},
mock_provider_response={
"id": "decision-9c26c028d7f4457e",
"object": "structured_decision",
"created": 1791517765,
"model": "Qwen/Qwen3-0.6B",
"answers": {
"is_urgent": {
"type": "choice",
"choice": "yes",
"probabilities": {"yes": 0.9669140238756562, "no": 0.03308597612434374},
"confidence": 0.9649145337527952,
}
},
"usage": {"input_tokens": 61, "output_tokens": 1},
"diagnostics": {"is_urgent": {"label_mass": 0.9979320910923947, "argmax_is_label": True}},
},
expected_litellm_response={
"id": "decision-9c26c028d7f4457e",
"object": "structured_decision",
"created": 1791517765,
"model": "Qwen/Qwen3-0.6B",
"answers": {
"is_urgent": {
"type": "choice",
"choice": "yes",
"probabilities": {"yes": 0.9669140238756562, "no": 0.03308597612434374},
"confidence": 0.9649145337527952,
}
},
"usage": {"input_tokens": 61, "output_tokens": 1},
"diagnostics": {"is_urgent": {"label_mass": 0.9979320910923947, "argmax_is_label": True}},
},
)
"""OpenAI Decisions client shape in, vLLM /v1/systemone body out. Mock reply captured live on 2026-10-09 from the same
vLLM build.
"""
QWEN3_0_6B_DECISIONS_TEST_CASE: Final = replace(
QWEN3_0_6B_TEST_CASE,
scenario="decisions",
litellm_endpoint="/v1/decisions",
litellm_request={
"model": "hosted_vllm/Qwen/Qwen3-0.6B",
"input": "The checkout page crashed twice today",
"questions": [
{
"type": "choice",
"name": "is_defect",
"instructions": "Is this a defect?",
"choices": [{"value": "yes"}, {"value": "no"}],
}
],
"cache": {"no-cache": True},
},
expected_provider_request={
"model": "Qwen/Qwen3-0.6B",
"state": "The checkout page crashed twice today",
"questions": {
"is_defect": {"type": "choice", "instructions": "Is this a defect?", "criteria": {"yes": None, "no": None}}
},
},
mock_provider_response={
"id": "decision-91ce41627dbf016f",
"object": "structured_decision",
"created": 1791577835,
"model": "Qwen/Qwen3-0.6B",
"answers": {
"is_defect": {
"type": "choice",
"choice": "yes",
"probabilities": {"yes": 0.8670357477770336, "no": 0.13296425222296632},
"confidence": 0.8669148648509282,
}
},
"usage": {"input_tokens": 43, "output_tokens": 1},
"diagnostics": {"is_defect": {"label_mass": 0.9998605790748358, "argmax_is_label": True}},
},
expected_litellm_response={
"model": "Qwen/Qwen3-0.6B",
"answers": [
{
"type": "choice",
"name": "is_defect",
"choice": "yes",
"probabilities": [
{"value": "yes", "probability": 0.8670357477770336},
{"value": "no", "probability": 0.13296425222296632},
],
"confidence": 0.8669148648509282,
}
],
"usage": {
"input_tokens": 43,
"input_tokens_details": {"cached_tokens": 0, "cache_write_tokens": 0},
"output_tokens": 1,
"output_tokens_details": {"reasoning_tokens": 0},
"total_tokens": 44,
},
},
)
"""vLLM only scores `choice` questions, so an OpenAI `predicate` goes out as `noul` and comes back as vLLM's 400,
which LiteLLM passes through as a 400.
"""
VLLM_NOUL_REJECTED: Final = {
"error": {
"message": "unknown question type 'noul'; supported: ['choice']",
"type": "BadRequestError",
"param": None,
"code": 400,
}
}
QWEN3_0_6B_PREDICATE_REJECTED_TEST_CASE: Final = replace(
QWEN3_0_6B_DECISIONS_TEST_CASE,
scenario="predicate_rejected",
litellm_request={
"model": "hosted_vllm/Qwen/Qwen3-0.6B",
"input": "The checkout page crashed twice today",
"questions": [{"type": "predicate", "name": "is_bug", "instructions": "Is this a bug?"}],
"cache": {"no-cache": True},
},
expected_provider_request={
"model": "Qwen/Qwen3-0.6B",
"state": "The checkout page crashed twice today",
"questions": {"is_bug": {"type": "noul", "instructions": "Is this a bug?"}},
},
mock_provider_status_code=400,
mock_provider_response=VLLM_NOUL_REJECTED,
expected_litellm_status_code=400,
expected_litellm_response={
"error": {
"message": f"litellm.BadRequestError: Hosted_vllmException - {json.dumps(VLLM_NOUL_REJECTED)}\n\n"
"LiteLLM: model group 'hosted_vllm/Qwen/Qwen3-0.6B' failed with the error above. No fallback was attempted.",
"type": "invalid_request_error",
"param": None,
"code": "400",
}
},
)

View file

@ -6,6 +6,100 @@ from integration.translation.case import TranslationTestCase
"""
TYPESAFE_JEV_1_13_TEST_CASE: Final = TranslationTestCase(
scenario="basic",
litellm_endpoint="/v1/decisions",
litellm_request={
"model": "openrouter/typesafe/jev-1.13",
"input": "Ticket (billing): The export job hangs at 99% and never finishes",
"questions": [
{"type": "predicate", "name": "defect", "instructions": "Is this a defect?"},
{
"type": "choice",
"name": "severity",
"instructions": "How severe is it?",
"choices": [
{"value": "low", "description": "cosmetic"},
{"value": "high", "description": "blocks users"},
],
},
{
"type": "score",
"name": "confidence",
"instructions": "How sure are you?",
"levels": [{"label": "unsure"}, {"label": "sure"}],
},
],
"cache": {"no-cache": True},
},
expected_provider_endpoint="/alpha/decisions",
expected_provider_headers={"authorization": "Bearer synthetic-openrouter-key", "content-type": "application/json"},
expected_provider_request={
"model": "typesafe/jev-1.13",
"state": "Ticket (billing): The export job hangs at 99% and never finishes",
"questions": {
"defect": {"type": "noul", "instructions": "Is this a defect?"},
"severity": {
"type": "choice",
"instructions": "How severe is it?",
"criteria": {"low": "cosmetic", "high": "blocks users"},
},
"confidence": {"type": "score", "instructions": "How sure are you?", "criteria": ["unsure", "sure"]},
},
},
mock_provider_response={
"model": "typesafe/jev-1.13-20260917",
"answers": {
"defect": {"type": "noul", "noul": 0.81},
"severity": {
"type": "choice",
"choice": "high",
"confidence": 0.99,
"probabilities": {"low": 0.01, "high": 0.99},
},
"confidence": {
"type": "score",
"score": 0.5,
"confidence": 0,
"legend": {"0": "unsure", "1": "sure"},
"probabilities": {"0": 0.5, "1": 0.5},
},
},
"usage": {"input_tokens": 377, "output_tokens": 62, "cost": 1.5834e-05},
"id": "gen-dec-1791323839-GA15kY0nt34oiJ7srfki",
"provider": "TypeSafe",
},
expected_litellm_response={
"model": "typesafe/jev-1.13-20260917",
"answers": [
{"type": "predicate", "name": "defect", "probability": 0.81},
{
"type": "choice",
"name": "severity",
"choice": "high",
"probabilities": [{"value": "low", "probability": 0.01}, {"value": "high", "probability": 0.99}],
"confidence": 0.99,
},
{
"type": "score",
"name": "confidence",
"score": 0.5,
"probabilities": [
{"value": 0, "label": "unsure", "probability": 0.5},
{"value": 1, "label": "sure", "probability": 0.5},
],
"confidence": 0,
},
],
"usage": {
"input_tokens": 377,
"input_tokens_details": {"cached_tokens": 0, "cache_write_tokens": 0},
"output_tokens": 62,
"output_tokens_details": {"reasoning_tokens": 0},
"total_tokens": 439,
},
},
)
TYPESAFE_JEV_1_13_SYSTEMONE_TEST_CASE: Final = TranslationTestCase(
scenario="systemone",
litellm_endpoint="/v1/systemone",
litellm_request={
"model": "openrouter/typesafe/jev-1.13",

View file

@ -6,6 +6,101 @@ from integration.translation.case import TranslationTestCase
"""
PPLX_DECIDER_V1_27B_TEST_CASE: Final = TranslationTestCase(
scenario="basic",
litellm_endpoint="/v1/decisions",
litellm_request={
"model": "perplexity/pplx-decider-v1-27b",
"input": "Ticket (billing): The export job hangs at 99% and never finishes",
"questions": [
{"type": "predicate", "name": "defect", "instructions": "Is this a defect?"},
{
"type": "choice",
"name": "severity",
"instructions": "How severe is it?",
"choices": [
{"value": "low", "description": "cosmetic"},
{"value": "high", "description": "blocks users"},
],
},
{
"type": "score",
"name": "confidence",
"instructions": "How sure are you?",
"levels": [{"label": "unsure"}, {"label": "sure"}],
},
],
"cache": {"no-cache": True},
},
expected_provider_endpoint="/v1/decisions",
expected_provider_headers={"authorization": "Bearer synthetic-perplexity-key", "content-type": "application/json"},
expected_provider_request={
"model": "pplx-decider-v1-27b",
"state": "Ticket (billing): The export job hangs at 99% and never finishes",
"questions": {
"defect": {"type": "noul", "instructions": "Is this a defect?"},
"severity": {
"type": "choice",
"instructions": "How severe is it?",
"criteria": {"low": "cosmetic", "high": "blocks users"},
},
"confidence": {"type": "score", "instructions": "How sure are you?", "criteria": ["unsure", "sure"]},
},
},
mock_provider_response={
"model": "pplx-decider-v1-27b",
"answers": {
"defect": {"type": "noul", "noul": 0.9989100737587077},
"severity": {
"type": "choice",
"choice": "high",
"confidence": 0.9964631215356778,
"probabilities": {"low": 0.0017684392321610232, "high": 0.9982315607678389},
},
"confidence": {
"type": "score",
"score": 0.07367392327139817,
"confidence": 0.8526521534572037,
"legend": {"0": "unsure", "1": "sure"},
"probabilities": {"0": 0.9263260767286018, "1": 0.07367392327139817},
},
},
"usage": {"input_tokens": 318, "output_tokens": 3},
},
expected_litellm_response={
"model": "pplx-decider-v1-27b",
"answers": [
{"type": "predicate", "name": "defect", "probability": 0.9989100737587077},
{
"type": "choice",
"name": "severity",
"choice": "high",
"probabilities": [
{"value": "low", "probability": 0.0017684392321610232},
{"value": "high", "probability": 0.9982315607678389},
],
"confidence": 0.9964631215356778,
},
{
"type": "score",
"name": "confidence",
"score": 0.07367392327139817,
"probabilities": [
{"value": 0, "label": "unsure", "probability": 0.9263260767286018},
{"value": 1, "label": "sure", "probability": 0.07367392327139817},
],
"confidence": 0.8526521534572037,
},
],
"usage": {
"input_tokens": 318,
"input_tokens_details": {"cached_tokens": 0, "cache_write_tokens": 0},
"output_tokens": 3,
"output_tokens_details": {"reasoning_tokens": 0},
"total_tokens": 321,
},
},
)
PPLX_DECIDER_V1_27B_SYSTEMONE_TEST_CASE: Final = TranslationTestCase(
scenario="systemone",
litellm_endpoint="/v1/systemone",
litellm_request={
"model": "perplexity/pplx-decider-v1-27b",

View file

@ -6,6 +6,40 @@ from integration.translation.case import TranslationTestCase
"""
STRANDS_DECIDER_2B_HOBSON_V19_TEST_CASE: Final = TranslationTestCase(
scenario="basic",
litellm_endpoint="/v1/decisions",
litellm_request={
"model": "strands_decider/strands-decider-2B-hobson-v19",
"input": "Help! My payouts have been failing for 3 days!",
"questions": [{"type": "predicate", "name": "is_urgent", "instructions": "Does this convey urgency?"}],
"cache": {"no-cache": True},
},
expected_provider_endpoint="/v1/systemone",
expected_provider_headers={"content-type": "application/json"},
expected_provider_request={
"model": "strands-decider-2B-hobson-v19",
"state": "Help! My payouts have been failing for 3 days!",
"questions": {"is_urgent": {"type": "noul", "instructions": "Does this convey urgency?"}},
},
mock_provider_response={
"model": "strands-decider-2B-hobson-v19",
"answers": {"is_urgent": {"type": "noul", "noul": 0.8277}},
"usage": {"input_tokens": 86, "output_tokens": 1},
"latency_ms": 140.03,
},
expected_litellm_response={
"model": "strands-decider-2B-hobson-v19",
"answers": [{"type": "predicate", "name": "is_urgent", "probability": 0.8277}],
"usage": {
"input_tokens": 86,
"input_tokens_details": {"cached_tokens": 0, "cache_write_tokens": 0},
"output_tokens": 1,
"output_tokens_details": {"reasoning_tokens": 0},
"total_tokens": 87,
},
},
)
STRANDS_DECIDER_2B_HOBSON_V19_SYSTEMONE_TEST_CASE: Final = TranslationTestCase(
scenario="systemone",
litellm_endpoint="/v1/systemone",
litellm_request={
"model": "strands_decider/strands-decider-2B-hobson-v19",

View file

@ -6,6 +6,98 @@ from integration.translation.case import TranslationTestCase
"""
JEV_1_13_0_TEST_CASE: Final = TranslationTestCase(
scenario="basic",
litellm_endpoint="/v1/decisions",
litellm_request={
"model": "typesafe/jev-1.13.0",
"input": "Ticket (billing): The export job hangs at 99% and never finishes",
"questions": [
{"type": "predicate", "name": "defect", "instructions": "Is this a defect?"},
{
"type": "choice",
"name": "severity",
"instructions": "How severe is it?",
"choices": [
{"value": "low", "description": "cosmetic"},
{"value": "high", "description": "blocks users"},
],
},
{
"type": "score",
"name": "confidence",
"instructions": "How sure are you?",
"levels": [{"label": "unsure"}, {"label": "sure"}],
},
],
"cache": {"no-cache": True},
},
expected_provider_endpoint="/v1/systemone",
expected_provider_headers={"authorization": "Bearer synthetic-typesafe-key", "content-type": "application/json"},
expected_provider_request={
"model": "jev-1.13.0",
"state": "Ticket (billing): The export job hangs at 99% and never finishes",
"questions": {
"defect": {"type": "noul", "instructions": "Is this a defect?"},
"severity": {
"type": "choice",
"instructions": "How severe is it?",
"criteria": {"low": "cosmetic", "high": "blocks users"},
},
"confidence": {"type": "score", "instructions": "How sure are you?", "criteria": ["unsure", "sure"]},
},
},
mock_provider_response={
"model": "jev-1.13.0",
"answers": {
"defect": {"type": "noul", "noul": 0.78},
"severity": {
"type": "choice",
"choice": "high",
"confidence": 0.99,
"probabilities": {"low": 0.01, "high": 0.99},
},
"confidence": {
"type": "score",
"score": 0.51,
"confidence": 0.03,
"legend": {"0": "unsure", "1": "sure"},
"probabilities": {"0": 0.49, "1": 0.51},
},
},
"usage": {"input_tokens": 377, "output_tokens": 62},
},
expected_litellm_response={
"model": "jev-1.13.0",
"answers": [
{"type": "predicate", "name": "defect", "probability": 0.78},
{
"type": "choice",
"name": "severity",
"choice": "high",
"probabilities": [{"value": "low", "probability": 0.01}, {"value": "high", "probability": 0.99}],
"confidence": 0.99,
},
{
"type": "score",
"name": "confidence",
"score": 0.51,
"probabilities": [
{"value": 0, "label": "unsure", "probability": 0.49},
{"value": 1, "label": "sure", "probability": 0.51},
],
"confidence": 0.03,
},
],
"usage": {
"input_tokens": 377,
"input_tokens_details": {"cached_tokens": 0, "cache_write_tokens": 0},
"output_tokens": 62,
"output_tokens_details": {"reasoning_tokens": 0},
"total_tokens": 439,
},
},
)
JEV_1_13_0_SYSTEMONE_TEST_CASE: Final = TranslationTestCase(
scenario="systemone",
litellm_endpoint="/v1/systemone",
litellm_request={
"model": "typesafe/jev-1.13.0",

View file

@ -2,10 +2,10 @@ import pytest
from integration._support.client import Gateway
from integration._support.provider import SharedProvider
from integration.translation.case import TranslationTestCase
from integration.translation.decisions.bases.cloudflare import CLEF_TEST_CASE
from integration.translation.decisions.bases.cloudflare import CLEF_TEST_CASE, CLEF_SYSTEMONE_TEST_CASE
from integration.translation.runner import assert_translation
@pytest.mark.parametrize("case", [CLEF_TEST_CASE], ids=lambda case: case.id)
@pytest.mark.parametrize("case", [CLEF_TEST_CASE, CLEF_SYSTEMONE_TEST_CASE], ids=lambda case: case.id)
def test_decisions_basic_cloudflare(case: TranslationTestCase, gateway: Gateway, provider: SharedProvider) -> None:
assert_translation(case, gateway, provider)

View file

@ -0,0 +1,19 @@
import pytest
from integration._support.client import Gateway
from integration._support.provider import SharedProvider
from integration.translation.case import TranslationTestCase
from integration.translation.decisions.bases.hosted_vllm import (
QWEN3_0_6B_DECISIONS_TEST_CASE,
QWEN3_0_6B_PREDICATE_REJECTED_TEST_CASE,
QWEN3_0_6B_TEST_CASE,
)
from integration.translation.runner import assert_translation
@pytest.mark.parametrize(
"case",
[QWEN3_0_6B_TEST_CASE, QWEN3_0_6B_DECISIONS_TEST_CASE, QWEN3_0_6B_PREDICATE_REJECTED_TEST_CASE],
ids=lambda case: case.id,
)
def test_decisions_basic_hosted_vllm(case: TranslationTestCase, gateway: Gateway, provider: SharedProvider) -> None:
assert_translation(case, gateway, provider)

View file

@ -2,10 +2,15 @@ import pytest
from integration._support.client import Gateway
from integration._support.provider import SharedProvider
from integration.translation.case import TranslationTestCase
from integration.translation.decisions.bases.openrouter import TYPESAFE_JEV_1_13_TEST_CASE
from integration.translation.decisions.bases.openrouter import (
TYPESAFE_JEV_1_13_TEST_CASE,
TYPESAFE_JEV_1_13_SYSTEMONE_TEST_CASE,
)
from integration.translation.runner import assert_translation
@pytest.mark.parametrize("case", [TYPESAFE_JEV_1_13_TEST_CASE], ids=lambda case: case.id)
@pytest.mark.parametrize(
"case", [TYPESAFE_JEV_1_13_TEST_CASE, TYPESAFE_JEV_1_13_SYSTEMONE_TEST_CASE], ids=lambda case: case.id
)
def test_decisions_basic_openrouter(case: TranslationTestCase, gateway: Gateway, provider: SharedProvider) -> None:
assert_translation(case, gateway, provider)

View file

@ -2,10 +2,15 @@ import pytest
from integration._support.client import Gateway
from integration._support.provider import SharedProvider
from integration.translation.case import TranslationTestCase
from integration.translation.decisions.bases.perplexity import PPLX_DECIDER_V1_27B_TEST_CASE
from integration.translation.decisions.bases.perplexity import (
PPLX_DECIDER_V1_27B_TEST_CASE,
PPLX_DECIDER_V1_27B_SYSTEMONE_TEST_CASE,
)
from integration.translation.runner import assert_translation
@pytest.mark.parametrize("case", [PPLX_DECIDER_V1_27B_TEST_CASE], ids=lambda case: case.id)
@pytest.mark.parametrize(
"case", [PPLX_DECIDER_V1_27B_TEST_CASE, PPLX_DECIDER_V1_27B_SYSTEMONE_TEST_CASE], ids=lambda case: case.id
)
def test_decisions_basic_perplexity(case: TranslationTestCase, gateway: Gateway, provider: SharedProvider) -> None:
assert_translation(case, gateway, provider)

View file

@ -2,10 +2,17 @@ import pytest
from integration._support.client import Gateway
from integration._support.provider import SharedProvider
from integration.translation.case import TranslationTestCase
from integration.translation.decisions.bases.strands_decider import STRANDS_DECIDER_2B_HOBSON_V19_TEST_CASE
from integration.translation.decisions.bases.strands_decider import (
STRANDS_DECIDER_2B_HOBSON_V19_TEST_CASE,
STRANDS_DECIDER_2B_HOBSON_V19_SYSTEMONE_TEST_CASE,
)
from integration.translation.runner import assert_translation
@pytest.mark.parametrize("case", [STRANDS_DECIDER_2B_HOBSON_V19_TEST_CASE], ids=lambda case: case.id)
@pytest.mark.parametrize(
"case",
[STRANDS_DECIDER_2B_HOBSON_V19_TEST_CASE, STRANDS_DECIDER_2B_HOBSON_V19_SYSTEMONE_TEST_CASE],
ids=lambda case: case.id,
)
def test_decisions_basic_strands_decider(case: TranslationTestCase, gateway: Gateway, provider: SharedProvider) -> None:
assert_translation(case, gateway, provider)

View file

@ -2,10 +2,10 @@ import pytest
from integration._support.client import Gateway
from integration._support.provider import SharedProvider
from integration.translation.case import TranslationTestCase
from integration.translation.decisions.bases.typesafe import JEV_1_13_0_TEST_CASE
from integration.translation.decisions.bases.typesafe import JEV_1_13_0_TEST_CASE, JEV_1_13_0_SYSTEMONE_TEST_CASE
from integration.translation.runner import assert_translation
@pytest.mark.parametrize("case", [JEV_1_13_0_TEST_CASE], ids=lambda case: case.id)
@pytest.mark.parametrize("case", [JEV_1_13_0_TEST_CASE, JEV_1_13_0_SYSTEMONE_TEST_CASE], ids=lambda case: case.id)
def test_decisions_basic_typesafe(case: TranslationTestCase, gateway: Gateway, provider: SharedProvider) -> None:
assert_translation(case, gateway, provider)

View file

@ -10,7 +10,7 @@ TRANSPORT_HEADERS: Final = frozenset({"host", "accept", "accept-encoding", "conn
def assert_translation(case: TranslationTestCase, gateway: Gateway, provider: SharedProvider) -> None:
provider.expect(Reply(body=json.dumps(case.mock_provider_response).encode()))
provider.expect(Reply(status=case.mock_provider_status_code, body=json.dumps(case.mock_provider_response).encode()))
response: Final = gateway.request("POST", case.litellm_endpoint, case.litellm_request)
received: Final = provider.received()
assert [(request.method, request.target) for request in received] == [("POST", case.expected_provider_endpoint)]

View file

@ -45,17 +45,25 @@ import litellm # noqa: E402 # litellm reads LITELLM_LOCAL_MODEL_COST_MAP at im
import litellm.router as litellm_router_module # noqa: E402 # same import-time dependency
import litellm.utils as litellm_utils_module # noqa: E402 # same import-time dependency
from litellm._logging import ALL_LOGGERS # noqa: E402 # same import-time dependency
from litellm.anthropic_beta_headers_manager import (
reload_beta_headers_config, # noqa: E402 # same import-time dependency
)
from litellm.litellm_core_utils.logging_worker import GLOBAL_LOGGING_WORKER # noqa: E402 # same import-time dependency
from litellm.anthropic_beta_headers_manager import reload_beta_headers_config # noqa: E402 # same import-time dependency
from litellm.litellm_core_utils.prompt_templates import factory as prompt_factory_module # noqa: E402 # same import-time dependency
from litellm.litellm_core_utils.prompt_templates import (
factory as prompt_factory_module, # noqa: E402 # same import-time dependency
)
from litellm.litellm_core_utils.prompt_templates import ( # noqa: E402 # same import-time dependency
image_handling as image_handling_module,
)
from litellm.llms.gemini.chat import transformation as gemini_chat_transformation_module # noqa: E402 # same import-time dependency
from litellm.llms.custom_httpx.async_client_cleanup import ( # noqa: E402 # same import-time dependency
close_litellm_async_clients,
)
from litellm.proxy.db import tool_registry_writer as tool_registry_writer_module # noqa: E402 # same import-time dependency
from litellm.llms.gemini.chat import (
transformation as gemini_chat_transformation_module, # noqa: E402 # same import-time dependency
)
from litellm.proxy.db import (
tool_registry_writer as tool_registry_writer_module, # noqa: E402 # same import-time dependency
)
LOOPBACK_HOSTS: Final = ["127.0.0.1", "::1", "localhost"]
AMBIENT_AZURE_CREDENTIAL_ENV_VARS: Final = (
@ -159,6 +167,31 @@ def _run_coroutine_if_needed(result: object) -> None:
loop.create_task(coroutine)
async def _complete_test_logging() -> None:
await GLOBAL_LOGGING_WORKER.flush()
await GLOBAL_LOGGING_WORKER.stop()
def _flush_completed_test_logging() -> None:
queue: Final = GLOBAL_LOGGING_WORKER._queue
if queue is None or queue._unfinished_tasks == 0:
return
worker_loop: Final = GLOBAL_LOGGING_WORKER._bound_loop
if worker_loop is None or worker_loop.is_closed():
_run_coroutine_if_needed(_complete_test_logging())
return
if worker_loop.is_running():
asyncio.run_coroutine_threadsafe(_complete_test_logging(), worker_loop).result()
return
worker_loop.run_until_complete(_complete_test_logging())
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call() -> Iterator[None]:
yield
_flush_completed_test_logging()
def _close_handler_if_needed(handler: object) -> None:
close: Final = getattr(handler, "close", None)
if not callable(close):

View file

@ -2,6 +2,8 @@ from __future__ import annotations
import asyncio
import json
import threading
from concurrent.futures import wait
from collections.abc import Mapping
from types import MappingProxyType
from typing import Final
@ -9,10 +11,16 @@ from typing import Final
import httpx
import pytest
import respx
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest
from botocore.credentials import Credentials
import litellm
from litellm.cost_calculator import get_response_cost_from_hidden_params
from litellm.constants import AWS_SIGNING_MAX_THREADS
from litellm.integrations.custom_logger import CustomLogger
from litellm.litellm_core_utils.logging_worker import GLOBAL_LOGGING_WORKER
from litellm.llms.bedrock.base_aws_llm import AWS_SIGNING_EXECUTOR
from litellm.types.decisions import (
ChoiceAnswer,
DecisionsResponse,
@ -21,6 +29,8 @@ from litellm.types.decisions import (
OpenAIDecisionResponse,
ScoreAnswer,
)
from litellm.types.utils import LlmProviders
from litellm.utils import ProviderConfigManager
_QUESTIONS: Final[Mapping[str, object]] = MappingProxyType(
{
@ -67,6 +77,20 @@ _STRANDS_RESPONSE: Final[Mapping[str, object]] = {
"usage": {"input_tokens": 216, "output_tokens": 3},
"latency_ms": 3722.17,
}
_DATABRICKS_ENDPOINT: Final = "databricks-openjev-qwen35-4b"
_DATABRICKS_RESPONSE: Final[Mapping[str, object]] = {
"model": "/mosaicml/local_model",
"answers": {
"is_defect": {"type": "noul", "noul": 0.9},
"sentiment": {
"type": "choice",
"choice": "positive",
"confidence": 0.8,
"probabilities": {"positive": 0.8, "negative": 0.2},
},
},
"usage": {"input_tokens": 214, "output_tokens": 0},
}
_PROVIDERS: Final[tuple[tuple[str, str, str, str], ...]] = (
(
"perplexity",
@ -139,7 +163,7 @@ async def test_adecisions_sends_the_provider_wire_contract(
) -> None:
route: Final = respx_mock.post(url).respond(json=_RESPONSE)
response: Final = await litellm.adecisions(
response: Final = await litellm.asystemone(
model=model,
state={"source": "unit-test"},
questions=_QUESTIONS,
@ -201,7 +225,7 @@ async def test_router_dispatches_typesafe_decisions_without_api_base(
)
upstream: Final = respx_mock.post("https://api.typesafe.ai/v1/systemone").respond(json=_RESPONSE)
response: Final = await router.adecisions(
response: Final = await router.asystemone(
model="jev",
state="router-test",
questions={
@ -232,7 +256,7 @@ async def test_router_dispatches_typesafe_decisions_without_api_base(
def test_decisions_uses_the_same_wire_contract_for_sync_calls(respx_mock: respx.MockRouter) -> None:
route: Final = respx_mock.post("https://api.perplexity.ai/v1/decisions").respond(json=_RESPONSE)
response: Final = litellm.decisions(
response: Final = litellm.systemone(
model="perplexity/pplx-decider-v1-27b",
state="review",
questions={"is_defect": {"type": "noul", "instructions": "Is this a defect?"}},
@ -252,7 +276,7 @@ def test_openrouter_response_keeps_provider_fields(respx_mock: respx.MockRouter)
}
respx_mock.post("https://openrouter.ai/api/alpha/decisions").respond(json=payload)
response: Final = litellm.decisions(
response: Final = litellm.systemone(
model="openrouter/typesafe/jev-1.13",
state="review",
questions=_QUESTIONS,
@ -340,7 +364,7 @@ async def test_decisions_cost_is_in_standard_logging_object(respx_mock: respx.Mo
litellm.callbacks = [recording_logger]
try:
await litellm.adecisions(
await litellm.asystemone(
model="perplexity/pplx-decider-v1-27b",
state="review",
questions={"is_defect": {"type": "noul", "instructions": "Is this a defect?"}},
@ -365,7 +389,7 @@ async def test_decisions_cost_is_in_standard_logging_object(respx_mock: respx.Mo
@pytest.mark.asyncio
async def test_unknown_provider_is_rejected_before_http(respx_mock: respx.MockRouter) -> None:
with pytest.raises(litellm.BadRequestError, match="LLM Provider NOT provided"):
await litellm.adecisions(
await litellm.asystemone(
model="unknown/jev-1.13",
state="review",
questions={"is_defect": {"type": "noul", "instructions": "Is this a defect?"}},
@ -378,7 +402,7 @@ async def test_unknown_provider_is_rejected_before_http(respx_mock: respx.MockRo
@pytest.mark.asyncio
async def test_provider_without_decisions_support_is_rejected_before_http(respx_mock: respx.MockRouter) -> None:
with pytest.raises(litellm.BadRequestError, match=r"Unknown Decisions provider 'anthropic'\. Supported providers"):
await litellm.adecisions(
await litellm.asystemone(
model="anthropic/claude-sonnet-4-5",
state="review",
questions={"is_defect": {"type": "noul", "instructions": "Is this a defect?"}},
@ -392,7 +416,7 @@ async def test_provider_without_decisions_support_is_rejected_before_http(respx_
async def test_empty_custom_provider_falls_back_to_the_model_prefix(respx_mock: respx.MockRouter) -> None:
route: Final = respx_mock.post("https://api.perplexity.ai/v1/decisions").respond(json=_RESPONSE)
response: Final = await litellm.adecisions(
response: Final = await litellm.asystemone(
model="perplexity/pplx-decider-v1-27b",
state="review",
questions={"is_defect": {"type": "noul", "instructions": "Is this a defect?"}},
@ -412,7 +436,7 @@ async def test_upstream_reply_without_answers_is_a_server_error(respx_mock: resp
)
with pytest.raises(litellm.InternalServerError, match="unexpected response"):
await litellm.adecisions(
await litellm.asystemone(
model="perplexity/pplx-decider-v1-27b",
state="review",
questions={"is_defect": {"type": "noul", "instructions": "Is this a defect?"}},
@ -440,7 +464,7 @@ async def test_router_sends_the_openrouter_deployment_key_when_the_provider_is_a
)
upstream: Final = respx_mock.post("https://egress.example/openrouter/alpha/decisions").respond(json=_RESPONSE)
await router.adecisions(
await router.asystemone(
model="jev",
state="review",
questions={"is_defect": {"type": "noul", "instructions": "Is this a defect?"}},
@ -454,7 +478,7 @@ async def test_router_sends_the_openrouter_deployment_key_when_the_provider_is_a
@pytest.mark.asyncio
async def test_invalid_question_is_rejected_before_http(respx_mock: respx.MockRouter) -> None:
with pytest.raises(litellm.BadRequestError, match="Invalid Decisions request"):
await litellm.adecisions(
await litellm.asystemone(
model="perplexity/pplx-decider-v1-27b",
state="review",
questions={"sentiment": {"type": "choice"}},
@ -471,7 +495,7 @@ def test_upstream_bad_request_maps_to_litellm_error(respx_mock: respx.MockRouter
)
with pytest.raises(litellm.BadRequestError):
litellm.decisions(
litellm.systemone(
model="perplexity/pplx-decider-v1-27b",
state="review",
questions={"is_defect": {"type": "noul", "instructions": "Is this a defect?"}},
@ -485,7 +509,7 @@ def test_unreachable_upstream_maps_to_a_connection_error(respx_mock: respx.MockR
)
with pytest.raises(litellm.APIConnectionError, match="PerplexityException - Cannot connect to host"):
litellm.decisions(
litellm.systemone(
model="perplexity/pplx-decider-v1-27b",
state="review",
questions={"is_defect": {"type": "noul", "instructions": "Is this a defect?"}},
@ -501,7 +525,7 @@ def test_server_key_is_sent_to_an_explicit_api_base(
monkeypatch.delenv("PERPLEXITY_API_KEY", raising=False)
route: Final = respx_mock.post("https://egress.example/perplexity/v1/decisions").respond(json=_RESPONSE)
litellm.decisions(
litellm.systemone(
model="perplexity/pplx-decider-v1-27b",
state="review",
questions={"is_defect": {"type": "noul", "instructions": "Is this a defect?"}},
@ -531,7 +555,7 @@ async def test_cloudflare_clef_resolves_model_and_response_envelope(
"https://api.cloudflare.com/client/v4/accounts/acct/ai/run/@cf/cloudflare/clef"
).respond(json=response_body)
response: Final = await litellm.adecisions(
response: Final = await litellm.asystemone(
model=model,
state="review",
questions={"is_defect": {"type": "noul", "instructions": "Is this a defect?"}},
@ -561,7 +585,7 @@ async def test_cloudflare_clef_flash_uses_flash_endpoint_and_request_model(
"https://api.cloudflare.com/client/v4/accounts/acct/ai/run/@cf/cloudflare/clef-flash"
).respond(json=_RESPONSE)
await litellm.adecisions(
await litellm.asystemone(
model="cloudflare/clef-flash",
state="review",
questions={"is_defect": {"type": "noul", "instructions": "Is this a defect?"}},
@ -583,7 +607,7 @@ async def test_cloudflare_api_base_from_env_uses_workers_ai_run_path(
"https://api.cloudflare.com/client/v4/accounts/acct/ai/run/@cf/cloudflare/clef"
).respond(json=_RESPONSE)
await litellm.adecisions(
await litellm.asystemone(
model="cloudflare/clef",
state="review",
questions={"is_defect": {"type": "noul", "instructions": "Is this a defect?"}},
@ -602,7 +626,7 @@ async def test_cloudflare_requires_account_id_or_api_base_before_http(
monkeypatch.setenv("CLOUDFLARE_API_KEY", "cloudflare-key")
with pytest.raises(litellm.BadRequestError, match="Missing CLOUDFLARE_ACCOUNT_ID - set CLOUDFLARE_ACCOUNT_ID"):
await litellm.adecisions(
await litellm.asystemone(
model="cloudflare/clef",
state="review",
questions={"is_defect": {"type": "noul", "instructions": "Is this a defect?"}},
@ -623,7 +647,7 @@ async def test_cloudflare_clef_cost_uses_the_model_cost_map(
json=_RESPONSE
)
response: Final = await litellm.adecisions(
response: Final = await litellm.asystemone(
model="cloudflare/clef",
state="review",
questions={"is_defect": {"type": "noul", "instructions": "Is this a defect?"}},
@ -648,7 +672,7 @@ async def test_strands_decider_requires_api_base_before_http(
monkeypatch.delenv("STRANDS_DECIDER_API_KEY", raising=False)
with pytest.raises(litellm.BadRequestError, match="api_base is required"):
await litellm.adecisions(
await litellm.asystemone(
model="strands_decider/strands-decider-2B-hobson-v19",
state="review",
questions={"severity": {"type": "score", "criteria": ["none", "low", "high"]}},
@ -666,7 +690,7 @@ async def test_strands_decider_without_key_preserves_response_extras(
monkeypatch.delenv("STRANDS_DECIDER_API_KEY", raising=False)
route: Final = respx_mock.post("https://strands.example/v1/systemone").respond(json=_STRANDS_RESPONSE)
response: Final = await litellm.adecisions(
response: Final = await litellm.asystemone(
model="strands_decider/strands-decider-2B-hobson-v19",
state="review",
questions={"severity": {"type": "score", "criteria": ["none", "low", "high"]}},
@ -681,6 +705,33 @@ async def test_strands_decider_without_key_preserves_response_extras(
assert severity.legend == {"0": "none", "1": "low", "2": "high"}
@pytest.mark.asyncio
async def test_strands_decider_plain_api_base_answers_while_every_aws_signing_thread_is_busy(
monkeypatch: pytest.MonkeyPatch,
respx_mock: respx.MockRouter,
) -> None:
monkeypatch.delenv("STRANDS_DECIDER_API_KEY", raising=False)
respx_mock.post("https://strands.example/v1/systemone").respond(json=_STRANDS_RESPONSE)
release: Final = threading.Event()
busy: Final = tuple(AWS_SIGNING_EXECUTOR.submit(release.wait) for _ in range(AWS_SIGNING_MAX_THREADS))
try:
response: Final = await asyncio.wait_for(
litellm.asystemone(
model="strands_decider/strands-decider-2B-hobson-v19",
state="review",
questions={"severity": {"type": "score", "criteria": ["none", "low", "high"]}},
api_base="https://strands.example",
),
timeout=5,
)
finally:
release.set()
wait(busy)
assert isinstance(response.answers["severity"], ScoreAnswer)
@pytest.mark.asyncio
async def test_strands_decider_uses_key_from_matching_environment_base(
monkeypatch: pytest.MonkeyPatch,
@ -690,7 +741,7 @@ async def test_strands_decider_uses_key_from_matching_environment_base(
monkeypatch.setenv("STRANDS_DECIDER_API_KEY", "strands-key")
route: Final = respx_mock.post("https://strands.example/v1/systemone").respond(json=_STRANDS_RESPONSE)
await litellm.adecisions(
await litellm.asystemone(
model="strands_decider/strands-decider-2B-hobson-v19",
state="review",
questions={"severity": {"type": "score", "criteria": ["none", "low", "high"]}},
@ -722,7 +773,7 @@ async def test_strands_decider_provider_resolution_and_router_dispatch(
)
route: Final = respx_mock.post("https://strands.example/v1/systemone").respond(json=_STRANDS_RESPONSE)
response: Final = await router.adecisions(
response: Final = await router.asystemone(
model="strands",
state="review",
questions={"severity": {"type": "score", "criteria": ["none", "low", "high"]}},
@ -733,6 +784,463 @@ async def test_strands_decider_provider_resolution_and_router_dispatch(
assert response.model == _STRANDS_RESPONSE["model"]
_RUNTIME_ARN: Final = "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/qa_decider-AbC123xyz0"
_RUNTIME_URL: Final = (
"https://bedrock-agentcore.us-east-1.amazonaws.com/runtimes/"
"arn%3Aaws%3Abedrock-agentcore%3Aus-east-1%3A123456789012%3Aruntime%2Fqa_decider-AbC123xyz0/invocations"
)
_RUNTIME_CREDENTIALS: Final = Credentials(access_key="AKIDEXAMPLE", secret_key="example-secret")
_SESSION_HEADER: Final = "x-amzn-bedrock-agentcore-runtime-session-id"
def _sigv4_verifies(request: httpx.Request) -> bool:
authorization: Final = request.headers["authorization"]
signed_names: Final = authorization.split("SignedHeaders=")[1].split(",")[0].split(";")
replayed: Final = AWSRequest(
method=request.method,
url=str(request.url),
data=request.content,
headers={name: request.headers[name] for name in signed_names},
)
replayed.context["timestamp"] = request.headers["x-amz-date"]
auth: Final = SigV4Auth(_RUNTIME_CREDENTIALS, "bedrock-agentcore", "us-east-1")
string_to_sign: Final = auth.string_to_sign(replayed, auth.canonical_request(replayed))
return authorization.endswith(f"Signature={auth.signature(string_to_sign, replayed)}")
@pytest.mark.parametrize("use_async", [True, False])
@pytest.mark.asyncio
async def test_strands_decider_runtime_arn_sends_a_sigv4_signed_invoke_agent_runtime_call(
monkeypatch: pytest.MonkeyPatch,
respx_mock: respx.MockRouter,
use_async: bool,
) -> None:
monkeypatch.delenv("STRANDS_DECIDER_API_KEY", raising=False)
monkeypatch.delenv("AWS_BEARER_TOKEN_BEDROCK", raising=False)
route: Final = respx_mock.post(_RUNTIME_URL).respond(json=_STRANDS_RESPONSE)
call_args: Final[Mapping[str, object]] = {
"model": "strands_decider/strands-decider-2B-hobson-v19",
"state": "review",
"questions": {"severity": {"type": "score", "criteria": ["none", "low", "high"]}},
"api_base": _RUNTIME_ARN,
"aws_access_key_id": _RUNTIME_CREDENTIALS.access_key,
"aws_secret_access_key": _RUNTIME_CREDENTIALS.secret_key,
}
response: Final = await litellm.asystemone(**call_args) if use_async else litellm.systemone(**call_args)
assert route.call_count == 1
sent: Final = route.calls[0].request
assert _sigv4_verifies(sent)
assert json.loads(sent.content)["model"] == "strands-decider-2B-hobson-v19"
assert sent.headers[_SESSION_HEADER].startswith("litellm-decider-")
assert response.model == _STRANDS_RESPONSE["model"]
@pytest.mark.asyncio
async def test_strands_decider_runtime_arn_router_deployment_answers_openai_format_calls(
monkeypatch: pytest.MonkeyPatch,
respx_mock: respx.MockRouter,
) -> None:
monkeypatch.delenv("STRANDS_DECIDER_API_KEY", raising=False)
router: Final = litellm.Router(
model_list=[
{
"model_name": "strands",
"litellm_params": {
"model": "strands_decider/strands-decider-2B-hobson-v19",
"api_base": _RUNTIME_ARN,
"aws_access_key_id": _RUNTIME_CREDENTIALS.access_key,
"aws_secret_access_key": _RUNTIME_CREDENTIALS.secret_key,
"aws_region_name": "eu-west-1",
"agentcore_runtime_session_id": "pooled-session-0123456789abcdef0123456",
},
}
]
)
route: Final = respx_mock.post(_RUNTIME_URL).respond(json=_STRANDS_RESPONSE)
response: Final = await router.adecisions(
model="strands",
input="review",
extra_headers={_SESSION_HEADER: "caller-session-0123456789abcdef01234567"},
questions=[
{
"type": "score",
"name": "severity",
"instructions": "How bad?",
"levels": [{"label": "none"}, {"label": "low"}, {"label": "high"}],
}
],
)
sent: Final = route.calls[0].request
assert _sigv4_verifies(sent)
assert sent.headers[_SESSION_HEADER] == "pooled-session-0123456789abcdef0123456"
assert isinstance(response, OpenAIDecisionResponse)
@pytest.mark.asyncio
async def test_strands_decider_runtime_arn_with_api_key_sends_the_bearer_token_unsigned(
monkeypatch: pytest.MonkeyPatch,
respx_mock: respx.MockRouter,
) -> None:
monkeypatch.delenv("STRANDS_DECIDER_API_KEY", raising=False)
route: Final = respx_mock.post(_RUNTIME_URL).respond(json=_STRANDS_RESPONSE)
await litellm.asystemone(
model="strands_decider/strands-decider-2B-hobson-v19",
state="review",
questions={"severity": {"type": "score", "criteria": ["none", "low", "high"]}},
api_base=_RUNTIME_ARN,
api_key="runtime-jwt",
)
sent: Final = route.calls[0].request
assert sent.headers["authorization"] == "Bearer runtime-jwt"
assert "x-amz-date" not in sent.headers
assert _SESSION_HEADER in sent.headers
@pytest.mark.parametrize(
("code", "error_class"),
[
("bad_request", litellm.BadRequestError),
("loading", litellm.ServiceUnavailableError),
("inference_error", litellm.InternalServerError),
],
)
@pytest.mark.asyncio
async def test_strands_decider_runtime_error_envelope_raises_instead_of_answering(
monkeypatch: pytest.MonkeyPatch,
respx_mock: respx.MockRouter,
code: str,
error_class: type[Exception],
) -> None:
monkeypatch.delenv("STRANDS_DECIDER_API_KEY", raising=False)
respx_mock.post(_RUNTIME_URL).respond(json={"error": {"code": code, "message": "at most 16 questions"}})
with pytest.raises(error_class, match="at most 16 questions"):
await litellm.asystemone(
model="strands_decider/strands-decider-2B-hobson-v19",
state="review",
questions={"severity": {"type": "score", "criteria": ["none", "low", "high"]}},
api_base=_RUNTIME_ARN,
aws_access_key_id=_RUNTIME_CREDENTIALS.access_key,
aws_secret_access_key=_RUNTIME_CREDENTIALS.secret_key,
)
@pytest.mark.asyncio
async def test_strands_decider_plain_api_base_error_envelope_stays_an_unexpected_response(
monkeypatch: pytest.MonkeyPatch,
respx_mock: respx.MockRouter,
) -> None:
monkeypatch.delenv("STRANDS_DECIDER_API_KEY", raising=False)
respx_mock.post("https://strands.example/v1/systemone").respond(
json={"error": {"code": "bad_request", "message": "at most 16 questions"}}
)
with pytest.raises(litellm.InternalServerError, match="returned an unexpected response"):
await litellm.asystemone(
model="strands_decider/strands-decider-2B-hobson-v19",
state="review",
questions={"severity": {"type": "score", "criteria": ["none", "low", "high"]}},
api_base="https://strands.example",
)
_VLLM_RESPONSE: Final[Mapping[str, object]] = {
"id": "systemone-4af3d2c1",
"object": "structured_decision",
"created": 1760012345,
"model": "Qwen/Qwen3-0.6B",
"answers": {
"is_defect": {
"type": "choice",
"choice": "yes",
"confidence": 0.91,
"probabilities": {"yes": 0.91, "no": 0.09},
}
},
"usage": {"input_tokens": 154, "output_tokens": 2},
"diagnostics": {"engine": "vllm", "logprobs_mode": "raw_logprobs"},
}
@pytest.mark.asyncio
async def test_hosted_vllm_requires_api_base_before_http(
monkeypatch: pytest.MonkeyPatch,
respx_mock: respx.MockRouter,
) -> None:
monkeypatch.delenv("HOSTED_VLLM_API_BASE", raising=False)
monkeypatch.delenv("HOSTED_VLLM_API_KEY", raising=False)
with pytest.raises(litellm.BadRequestError, match="api_base is required"):
await litellm.asystemone(
model="hosted_vllm/Qwen/Qwen3-0.6B",
state="review",
questions={"is_defect": {"type": "choice", "criteria": {"yes": None, "no": None}}},
)
assert len(respx_mock.calls) == 0
@pytest.mark.asyncio
async def test_hosted_vllm_without_key_sends_no_authorization_and_preserves_response_extras(
monkeypatch: pytest.MonkeyPatch,
respx_mock: respx.MockRouter,
) -> None:
monkeypatch.delenv("HOSTED_VLLM_API_BASE", raising=False)
monkeypatch.delenv("HOSTED_VLLM_API_KEY", raising=False)
route: Final = respx_mock.post("http://vllm.local:8000/v1/systemone").respond(json=_VLLM_RESPONSE)
response: Final = await litellm.asystemone(
model="hosted_vllm/Qwen/Qwen3-0.6B",
state="The package arrived broken.",
questions={"is_defect": {"type": "choice", "criteria": {"yes": None, "no": None}}},
api_base="http://vllm.local:8000",
)
assert route.called
assert "authorization" not in respx_mock.calls[0].request.headers
assert response.model_extra["id"] == _VLLM_RESPONSE["id"]
assert response.model_extra["object"] == _VLLM_RESPONSE["object"]
assert response.model_extra["diagnostics"] == _VLLM_RESPONSE["diagnostics"]
@pytest.mark.asyncio
async def test_hosted_vllm_uses_key_and_base_from_environment(
monkeypatch: pytest.MonkeyPatch,
respx_mock: respx.MockRouter,
) -> None:
monkeypatch.setenv("HOSTED_VLLM_API_BASE", "http://vllm.local:8000/v1")
monkeypatch.setenv("HOSTED_VLLM_API_KEY", "vllm-key")
route: Final = respx_mock.post("http://vllm.local:8000/v1/systemone").respond(json=_VLLM_RESPONSE)
await litellm.asystemone(
model="hosted_vllm/Qwen/Qwen3-0.6B",
state="The package arrived broken.",
questions={"is_defect": {"type": "choice", "criteria": {"yes": None, "no": None}}},
)
assert route.called
assert respx_mock.calls[0].request.headers["authorization"] == "Bearer vllm-key"
@pytest.mark.asyncio
async def test_hosted_vllm_api_base_with_v1_suffix_posts_to_v1_systemone(
monkeypatch: pytest.MonkeyPatch,
respx_mock: respx.MockRouter,
) -> None:
monkeypatch.delenv("HOSTED_VLLM_API_BASE", raising=False)
monkeypatch.delenv("HOSTED_VLLM_API_KEY", raising=False)
route: Final = respx_mock.post("http://vllm.local:8000/v1/systemone").respond(json=_VLLM_RESPONSE)
await litellm.asystemone(
model="hosted_vllm/Qwen/Qwen3-0.6B",
state="The package arrived broken.",
questions={"is_defect": {"type": "choice", "criteria": {"yes": None, "no": None}}},
api_base="http://vllm.local:8000/v1",
)
assert route.called
@pytest.mark.asyncio
async def test_hosted_vllm_choice_question_goes_out_as_a_jev_choice_with_the_served_model_name(
monkeypatch: pytest.MonkeyPatch,
respx_mock: respx.MockRouter,
) -> None:
monkeypatch.delenv("HOSTED_VLLM_API_BASE", raising=False)
monkeypatch.delenv("HOSTED_VLLM_API_KEY", raising=False)
route: Final = respx_mock.post("http://vllm.local:8000/v1/systemone").respond(json=_VLLM_RESPONSE)
await litellm.asystemone(
model="hosted_vllm/Qwen/Qwen3-0.6B",
state="The package arrived broken.",
questions={
"is_defect": {
"type": "choice",
"instructions": "Is this a defect?",
"criteria": {"yes": "defective", "no": "intact"},
}
},
api_base="http://vllm.local:8000",
)
assert route.called
assert json.loads(respx_mock.calls[0].request.content) == {
"model": "Qwen/Qwen3-0.6B",
"state": "The package arrived broken.",
"questions": {
"is_defect": {
"type": "choice",
"instructions": "Is this a defect?",
"criteria": {"yes": "defective", "no": "intact"},
}
},
}
@pytest.mark.asyncio
async def test_databricks_requires_api_base_before_http(
monkeypatch: pytest.MonkeyPatch,
respx_mock: respx.MockRouter,
) -> None:
monkeypatch.delenv("DATABRICKS_API_BASE", raising=False)
monkeypatch.setenv("DATABRICKS_API_KEY", "dapi-key")
with pytest.raises(litellm.BadRequestError, match="DATABRICKS_API_BASE"):
await litellm.asystemone(
model=f"databricks/{_DATABRICKS_ENDPOINT}",
state="review",
questions={"is_defect": {"type": "noul", "instructions": "Is this a defect?"}},
)
assert len(respx_mock.calls) == 0
@pytest.mark.asyncio
@pytest.mark.parametrize(
"endpoint_name", ["serving-endpoints/openjev", "openjev?x=1", "openjev#frag", "..", "open jev"]
)
async def test_databricks_rejects_a_serving_endpoint_name_that_would_rewrite_the_url(
monkeypatch: pytest.MonkeyPatch,
respx_mock: respx.MockRouter,
endpoint_name: str,
) -> None:
monkeypatch.setenv("DATABRICKS_API_BASE", "https://workspace.example/serving-endpoints")
monkeypatch.setenv("DATABRICKS_API_KEY", "dapi-key")
with pytest.raises(litellm.BadRequestError, match="bare endpoint name"):
await litellm.asystemone(
model=f"databricks/{endpoint_name}",
state="review",
questions={"is_defect": {"type": "noul", "instructions": "Is this a defect?"}},
)
assert len(respx_mock.calls) == 0
@pytest.mark.asyncio
async def test_databricks_requires_a_key_before_http(
monkeypatch: pytest.MonkeyPatch,
respx_mock: respx.MockRouter,
) -> None:
monkeypatch.setenv("DATABRICKS_API_BASE", "https://workspace.example/serving-endpoints")
monkeypatch.delenv("DATABRICKS_API_KEY", raising=False)
monkeypatch.delenv("DATABRICKS_TOKEN", raising=False)
with pytest.raises(litellm.AuthenticationError, match="Missing API key for Decisions provider 'databricks'"):
await litellm.asystemone(
model=f"databricks/{_DATABRICKS_ENDPOINT}",
state="review",
questions={"is_defect": {"type": "noul", "instructions": "Is this a defect?"}},
)
assert len(respx_mock.calls) == 0
@pytest.mark.asyncio
@pytest.mark.parametrize(
"api_base", ["https://workspace.example/serving-endpoints", "https://workspace.example/serving-endpoints/"]
)
async def test_databricks_posts_the_jev_body_to_the_serving_endpoint_invocations_route(
monkeypatch: pytest.MonkeyPatch,
respx_mock: respx.MockRouter,
api_base: str,
) -> None:
monkeypatch.setenv("DATABRICKS_API_BASE", api_base)
monkeypatch.delenv("DATABRICKS_API_KEY", raising=False)
monkeypatch.setenv("DATABRICKS_TOKEN", "dapi-token")
route: Final = respx_mock.post(
f"https://workspace.example/serving-endpoints/{_DATABRICKS_ENDPOINT}/invocations"
).respond(json=_DATABRICKS_RESPONSE)
questions: Final = {
"is_defect": {"type": "noul", "instructions": "Is this a defect?"},
"sentiment": {"type": "choice", "criteria": {"positive": None, "negative": "unhappy"}},
}
response: Final = await litellm.asystemone(
model=f"databricks/{_DATABRICKS_ENDPOINT}",
state={"ticket": "export hangs"},
questions=questions,
)
assert route.called
sent: Final = respx_mock.calls[0].request
assert sent.headers["authorization"] == "Bearer dapi-token"
assert json.loads(sent.content) == {
"model": _DATABRICKS_ENDPOINT,
"state": {"ticket": "export hangs"},
"questions": questions,
}
assert response.model == "/mosaicml/local_model"
assert isinstance(response.answers["is_defect"], NoulAnswer)
assert isinstance(response.answers["sentiment"], ChoiceAnswer)
assert response.hidden_params["custom_llm_provider"] == "databricks"
assert response.hidden_params["model"] == f"databricks/{_DATABRICKS_ENDPOINT}"
@pytest.mark.asyncio
async def test_databricks_router_deployment_sends_its_own_key_to_its_own_base(
monkeypatch: pytest.MonkeyPatch,
respx_mock: respx.MockRouter,
) -> None:
monkeypatch.setenv("DATABRICKS_API_BASE", "https://other-workspace.example/serving-endpoints")
monkeypatch.setenv("DATABRICKS_API_KEY", "env-key-stays-home")
router: Final = litellm.Router(
model_list=[
{
"model_name": "decider",
"litellm_params": {
"model": f"databricks/{_DATABRICKS_ENDPOINT}",
"api_base": "https://workspace.example/serving-endpoints",
"api_key": "deployment-key",
},
}
]
)
route: Final = respx_mock.post(
f"https://workspace.example/serving-endpoints/{_DATABRICKS_ENDPOINT}/invocations"
).respond(json=_DATABRICKS_RESPONSE)
response: Final = await router.asystemone(
model="decider",
state="review",
questions={"is_defect": {"type": "noul", "instructions": "Is this a defect?"}},
)
assert route.called
assert respx_mock.calls[0].request.headers["authorization"] == "Bearer deployment-key"
assert response.answers["is_defect"] == NoulAnswer(type="noul", noul=0.9)
_PROVIDERS_WHOSE_CHAT_MODELS_ALSO_DECIDE: Final = frozenset(
{LlmProviders.OPENAI.value, LlmProviders.OPENROUTER.value, LlmProviders.HOSTED_VLLM.value}
)
_DEDICATED_DECIDER_PROVIDERS: Final[tuple[str, ...]] = tuple(
sorted(
provider.value
for provider in LlmProviders
if provider.value not in _PROVIDERS_WHOSE_CHAT_MODELS_ALSO_DECIDE
and ProviderConfigManager.get_provider_decisions_config(model="", provider=provider) is not None
)
)
@pytest.mark.parametrize("provider", _DEDICATED_DECIDER_PROVIDERS)
def test_every_dedicated_decider_provider_ships_an_evaluation_mode_cost_map_entry(provider: str) -> None:
evaluation_entries: Final = tuple(
name
for name, info in litellm.model_cost.items()
if isinstance(info, Mapping) and info.get("litellm_provider") == provider and info.get("mode") == "evaluation"
)
assert evaluation_entries, f"a {provider} decider would be health-checked as chat without a mode: evaluation entry"
@pytest.mark.asyncio
@pytest.mark.parametrize(
("api_base", "url"),
@ -752,7 +1260,7 @@ async def test_openai_decisions_translate_systemone_to_the_openai_wire_contract_
monkeypatch.delenv("OPENAI_API_BASE", raising=False)
route: Final = respx_mock.post(url).respond(json=_OPENAI_RESPONSE)
response: Final = await litellm.adecisions(
response: Final = await litellm.asystemone(
model="openai/gpt-6-luna",
state="The package arrived broken.",
questions={
@ -837,7 +1345,7 @@ def test_openai_decisions_use_the_same_settings_as_other_openai_calls(
monkeypatch.setenv(name, value)
route: Final = respx_mock.post(url).respond(json=_OPENAI_RESPONSE)
litellm.decisions(
litellm.systemone(
model="openai/gpt-6-luna",
state="review",
questions={"is_defect": {"type": "noul", "instructions": "Is this a defect?"}},
@ -900,17 +1408,8 @@ async def test_openai_format_calls_to_a_systemone_provider_get_openai_format_ans
},
"cannot serve this request",
),
(
"openai/gpt-6-luna",
{
"state": "review",
"input": "review",
"questions": [{"type": "predicate", "instructions": "Is this a defect?"}],
},
"not both",
),
),
ids=("image_to_systemone_provider", "state_and_input"),
ids=("image_to_systemone_provider",),
)
async def test_requests_a_provider_cannot_serve_are_rejected_before_http(
respx_mock: respx.MockRouter,
@ -924,6 +1423,175 @@ async def test_requests_a_provider_cannot_serve_are_rejected_before_http(
assert len(respx_mock.calls) == 0
@pytest.mark.asyncio
async def test_openrouter_decisions_uses_provider_reported_cost_without_cost_map(
respx_mock: respx.MockRouter,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delitem(litellm.model_cost, "openrouter/typesafe/jev-1.13", raising=False)
cost: Final = 1.1802e-5
route: Final = respx_mock.post("https://openrouter.ai/api/alpha/decisions").respond(
json={
"model": "typesafe/jev-1.13",
"answers": _RESPONSE["answers"],
"usage": {
"input_tokens": _INPUT_TOKENS,
"output_tokens": _OUTPUT_TOKENS,
"cost": cost,
},
}
)
response: Final = await litellm.asystemone(
model="openrouter/typesafe/jev-1.13",
state={"source": "unit-test"},
questions=_QUESTIONS,
api_key="caller-key",
)
assert route.called
assert get_response_cost_from_hidden_params(response.hidden_params) == cost
_SAFETY_IDENTIFIER: Final = "end-user-7"
_PREDICATE_QUESTIONS: Final[tuple[Mapping[str, object], ...]] = (
{"type": "predicate", "name": "is_defect", "instructions": "Is this a defect?"},
)
_PREDICATE_REQUESTS: Final[tuple[tuple[str, Mapping[str, object]], ...]] = (
("decisions", MappingProxyType({"input": "review", "questions": _PREDICATE_QUESTIONS})),
(
"systemone",
MappingProxyType(
{"state": "review", "questions": {"is_defect": {"type": "noul", "instructions": "Is this a defect?"}}}
),
),
)
_PREDICATE_REQUEST_IDS: Final = ("decisions", "systemone")
@pytest.mark.parametrize(("api", "request_kwargs"), _PREDICATE_REQUESTS, ids=_PREDICATE_REQUEST_IDS)
def test_safety_identifier_is_refused_before_http_when_the_provider_cannot_take_it(
api: str, request_kwargs: Mapping[str, object], respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(litellm, "drop_params", False)
route: Final = respx_mock.post("https://api.perplexity.ai/v1/decisions").respond(json=_RESPONSE)
with pytest.raises(litellm.UnsupportedParamsError, match=r"safety_identifier.*drop_params") as caught:
getattr(litellm, api)(
model="perplexity/pplx-decider-v1-27b",
safety_identifier=_SAFETY_IDENTIFIER,
api_key="caller-key",
**request_kwargs,
)
assert caught.value.status_code == 400
assert not route.called
@pytest.mark.asyncio
@pytest.mark.parametrize("scope", ("call", "global"))
async def test_safety_identifier_is_dropped_from_the_wire_under_drop_params(
scope: str, respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(litellm, "drop_params", scope == "global")
route: Final = respx_mock.post("https://api.perplexity.ai/v1/decisions").respond(json=_RESPONSE)
response: Final = await litellm.adecisions(
model="perplexity/pplx-decider-v1-27b",
input="review",
questions=_PREDICATE_QUESTIONS,
safety_identifier=_SAFETY_IDENTIFIER,
api_key="caller-key",
**({"drop_params": True} if scope == "call" else {}),
)
assert route.called
assert json.loads(respx_mock.calls[0].request.content) == {
"model": "pplx-decider-v1-27b",
"state": "review",
"questions": {"is_defect": {"type": "noul", "instructions": "Is this a defect?"}},
}
assert isinstance(response, OpenAIDecisionResponse)
assert [answer.model_dump(mode="json") for answer in response.answers] == [
{"type": "predicate", "name": "is_defect", "probability": 0.9}
]
@pytest.mark.asyncio
@pytest.mark.parametrize(("api", "request_kwargs"), _PREDICATE_REQUESTS, ids=_PREDICATE_REQUEST_IDS)
@pytest.mark.parametrize("drop_params", (False, True), ids=("strict", "drop_params"))
async def test_safety_identifier_reaches_openai_whether_or_not_params_are_dropped(
drop_params: bool,
api: str,
request_kwargs: Mapping[str, object],
respx_mock: respx.MockRouter,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(litellm, "drop_params", drop_params)
monkeypatch.setattr(litellm, "api_base", None)
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
monkeypatch.delenv("OPENAI_API_BASE", raising=False)
route: Final = respx_mock.post("https://api.openai.com/v1/decisions").respond(json=_OPENAI_RESPONSE)
await getattr(litellm, f"a{api}")(
model="openai/gpt-6-luna",
safety_identifier=_SAFETY_IDENTIFIER,
api_key="caller-key",
**request_kwargs,
)
assert route.called
assert json.loads(respx_mock.calls[0].request.content) == {
"model": "gpt-6-luna",
"input": "review",
"questions": [{"type": "predicate", "name": "is_defect", "instructions": "Is this a defect?"}],
"safety_identifier": _SAFETY_IDENTIFIER,
}
@pytest.mark.asyncio
@pytest.mark.parametrize(("api", "request_kwargs"), _PREDICATE_REQUESTS, ids=_PREDICATE_REQUEST_IDS)
async def test_non_string_safety_identifier_is_rejected_before_http(
api: str, request_kwargs: Mapping[str, object], respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(litellm, "drop_params", False)
route: Final = respx_mock.post("https://api.openai.com/v1/decisions").respond(json=_OPENAI_RESPONSE)
invalid_safety_identifier: Final[Mapping[str, object]] = MappingProxyType({"safety_identifier": 7})
with pytest.raises(litellm.BadRequestError, match=r"(?s)Invalid Decisions request.*safety_identifier"):
await getattr(litellm, f"a{api}")(
model="openai/gpt-6-luna", api_key="caller-key", **request_kwargs, **invalid_safety_identifier
)
assert not route.called
@pytest.mark.asyncio
@pytest.mark.parametrize(("api", "request_kwargs"), _PREDICATE_REQUESTS, ids=_PREDICATE_REQUEST_IDS)
async def test_non_string_safety_identifier_is_dropped_from_the_wire_under_drop_params(
api: str, request_kwargs: Mapping[str, object], respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(litellm, "drop_params", False)
monkeypatch.setattr(litellm, "api_base", None)
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
monkeypatch.delenv("OPENAI_API_BASE", raising=False)
route: Final = respx_mock.post("https://api.openai.com/v1/decisions").respond(json=_OPENAI_RESPONSE)
dropped_safety_identifier: Final[Mapping[str, object]] = MappingProxyType(
{"safety_identifier": 7, "drop_params": True}
)
await getattr(litellm, f"a{api}")(
model="openai/gpt-6-luna", api_key="caller-key", **request_kwargs, **dropped_safety_identifier
)
assert route.called
assert json.loads(respx_mock.calls[0].request.content) == {
"model": "gpt-6-luna",
"input": "review",
"questions": [{"type": "predicate", "name": "is_defect", "instructions": "Is this a defect?"}],
}
@pytest.mark.asyncio
@pytest.mark.parametrize(
"api_base",
@ -949,7 +1617,7 @@ async def test_azure_ai_decision_posts_deployment_to_foundry_systemone_route(
json={**_RESPONSE, "model": "microsoft-decision-1"}
)
response: Final = await litellm.adecisions(
response: Final = await litellm.asystemone(
model="azure_ai/decision-1",
state="review",
questions={"is_defect": {"type": "noul", "instructions": "Is this a defect?"}},
@ -981,7 +1649,7 @@ async def test_azure_ai_decision_reads_foundry_env_and_keeps_gateway_path_prefix
json=_RESPONSE
)
await litellm.adecisions(
await litellm.asystemone(
model="azure_ai/decision-1",
state="review",
questions={"is_defect": {"type": "noul", "instructions": "Is this a defect?"}},
@ -1000,7 +1668,7 @@ async def test_azure_ai_decision_requires_api_base_before_http(
monkeypatch.setenv("AZURE_AI_API_KEY", "foundry-key")
with pytest.raises(litellm.BadRequestError, match="Missing AZURE_AI_API_BASE"):
await litellm.adecisions(
await litellm.asystemone(
model="azure_ai/decision-1",
state="review",
questions={"is_defect": {"type": "noul", "instructions": "Is this a defect?"}},

View file

@ -679,6 +679,81 @@ async def test_ahealth_check_probes_strands_through_decisions_without_mode(
assert "authorization" not in upstream.calls[0].request.headers
@pytest.mark.asyncio
async def test_ahealth_check_probes_hosted_vllm_with_a_choice_question(
monkeypatch: pytest.MonkeyPatch,
respx_mock: respx.MockRouter,
) -> None:
monkeypatch.delenv("HOSTED_VLLM_API_KEY", raising=False)
monkeypatch.delenv("HOSTED_VLLM_API_BASE", raising=False)
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
litellm.in_memory_llm_clients_cache.flush_cache()
upstream: Final = respx_mock.post("http://vllm.local:8000/v1/systemone").respond(
json={
"model": "Qwen/Qwen3-0.6B",
"answers": {
"reachable": {
"type": "choice",
"choice": "yes",
"confidence": 1.0,
"probabilities": {"yes": 1.0, "no": 0.0},
}
},
"usage": {"input_tokens": 12, "output_tokens": 1},
}
)
result: Final = await ahealth_check(
{
"model": "hosted_vllm/Qwen/Qwen3-0.6B",
"api_base": "http://vllm.local:8000",
},
mode="evaluation",
)
assert "error" not in result, result
assert upstream.called
sent: Final = json.loads(upstream.calls[0].request.content)
assert sent["questions"] == {
"reachable": {
"type": "choice",
"instructions": "Is the service reachable?",
"criteria": {"yes": None, "no": None},
}
}
@pytest.mark.asyncio
async def test_ahealth_check_keeps_the_noul_probe_for_other_decisions_providers(
monkeypatch: pytest.MonkeyPatch,
respx_mock: respx.MockRouter,
) -> None:
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
litellm.in_memory_llm_clients_cache.flush_cache()
upstream: Final = respx_mock.post("https://api.typesafe.ai/v1/systemone").respond(
json={
"model": "jev-1.13",
"answers": {"reachable": {"type": "noul", "noul": 1.0}},
"usage": {"input_tokens": 12, "output_tokens": 1},
}
)
result: Final = await ahealth_check(
{
"model": "typesafe/jev-1.13",
"api_key": "sk-test",
},
mode="evaluation",
)
assert "error" not in result, result
assert upstream.called
sent: Final = json.loads(upstream.calls[0].request.content)
assert sent["questions"] == {
"reachable": {"type": "noul", "instructions": "Is the service reachable?"}
}
@pytest.mark.parametrize(
("model", "custom_llm_provider", "expected"),
(

View file

@ -471,6 +471,7 @@ def test_default_max_retries_env_var_reaches_azure_sdk_client():
"llm_passthrough_route",
"asearch",
"adecisions",
"asystemone",
"avector_store_create",
"avector_store_search",
"acreate_skill",

View file

@ -0,0 +1,56 @@
from typing import Final
import pytest
from litellm.llms.databricks.decisions.transformation import DatabricksDecisionsConfig
_BASE: Final = "https://workspace.example/serving-endpoints"
_CONFIG: Final = DatabricksDecisionsConfig()
@pytest.mark.parametrize("api_base", [_BASE, f"{_BASE}/"])
def test_the_complete_url_is_the_serving_endpoint_invocations_route(api_base: str) -> None:
assert _CONFIG.get_complete_url(api_base, "openjev") == f"{_BASE}/openjev/invocations"
@pytest.mark.parametrize(
"name",
["", "serving-endpoints/openjev", "openjev?x=1", "openjev#frag", ".", "..", ".openjev", "open jev", "openjev%2Fx"],
)
def test_a_name_that_would_rewrite_the_url_is_rejected_before_any_request(name: str) -> None:
with pytest.raises(ValueError, match="bare endpoint name"):
_ = _CONFIG.get_complete_url(_BASE, name)
with pytest.raises(ValueError, match="bare endpoint name"):
_ = _CONFIG.canonical_model(name)
def test_connection_prefers_the_configured_values_over_the_environment(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("DATABRICKS_API_BASE", "https://env.example/serving-endpoints")
monkeypatch.setenv("DATABRICKS_API_KEY", "dapi-env")
connection: Final = _CONFIG.connection(f"{_BASE}/", "dapi-configured")
assert (connection.api_base, connection.api_key) == (_BASE, "dapi-configured")
def test_connection_falls_back_to_the_workspace_token_when_the_api_key_variable_is_unset(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("DATABRICKS_API_BASE", _BASE)
monkeypatch.delenv("DATABRICKS_API_KEY", raising=False)
monkeypatch.setenv("DATABRICKS_TOKEN", "dapi-token")
connection: Final = _CONFIG.connection(None, None)
assert (connection.api_base, connection.api_key) == (_BASE, "dapi-token")
def test_connection_never_sends_the_environment_key_to_a_configured_base(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("DATABRICKS_API_BASE", _BASE)
monkeypatch.setenv("DATABRICKS_API_KEY", "dapi-env")
with pytest.raises(ValueError, match="DATABRICKS_API_BASE"):
_ = _CONFIG.connection("https://collector.example/serving-endpoints", None)
def test_classifier_response_accounts_the_serving_endpoint_instead_of_the_container_model() -> None:
body: Final = {"model": "/mosaicml/local_model", "answers": {}, "usage": {"input_tokens": 3, "output_tokens": 0}}
normalized: Final = _CONFIG.classifier_response(body, "openjev")
assert normalized["model"] == "openjev"
assert normalized["usage"] == body["usage"]
assert body["model"] == "/mosaicml/local_model"

View file

@ -96,6 +96,15 @@ def test_openrouter_extra_body_transformation():
]
def test_openrouter_supported_params_include_reasoning_for_unmapped_model():
supported_params = OpenrouterConfig().get_supported_openai_params(
"openrouter/unmapped-model"
)
assert "reasoning_effort" in supported_params
assert "thinking" in supported_params
def test_openrouter_cache_control_flag_removal():
transformed_request = OpenrouterConfig().transform_request(
model="openrouter/deepseek/deepseek-chat",
@ -537,17 +546,15 @@ def test_openrouter_reasoning_models_allow_reasoning_effort_param():
assert supported_params.count("reasoning_effort") == 1
def test_openrouter_non_reasoning_models_do_not_add_reasoning_effort():
"""
Models without reasoning support should not gain reasoning-specific params.
"""
def test_openrouter_non_reasoning_models_support_reasoning_params():
config = OpenrouterConfig()
supported_params = config.get_supported_openai_params(
model="openrouter/anthropic/claude-3-5-haiku"
)
assert "reasoning_effort" not in supported_params
assert "reasoning_effort" in supported_params
assert "thinking" in supported_params
def test_openrouter_reasoning_effort_max_maps_to_xhigh():

View file

@ -9,12 +9,22 @@ reasoning.encrypted_content for multi-turn stateless workflows.
Related issue: https://github.com/BerriAI/litellm/issues/22189
"""
from unittest.mock import MagicMock
import httpx
import pytest
import litellm
from litellm.cost_calculator import get_response_cost_from_hidden_params
from litellm.litellm_core_utils.core_helpers import RESPONSE_COST_HEADER
from litellm.llms.openrouter.responses.transformation import (
OpenRouterResponsesAPIConfig,
)
from litellm.types.llms.openai import (
ResponseCompletedEvent,
ResponseFailedEvent,
ResponseIncompleteEvent,
)
from litellm.types.utils import LlmProviders
from litellm.utils import ProviderConfigManager
@ -116,3 +126,66 @@ class TestOpenRouterResponsesAPIRegistration:
# The URL should point to OpenRouter's responses endpoint
url = config.get_complete_url(api_base=None, litellm_params={})
assert "/responses" in url
@pytest.mark.parametrize("cost", [1.23e-5, None])
def test_openrouter_responses_returns_provider_reported_cost(cost):
usage: dict[str, object] = {
"input_tokens": 2,
"output_tokens": 3,
"total_tokens": 5,
}
if cost is not None:
usage["cost"] = cost
response = OpenRouterResponsesAPIConfig().transform_response_api_response(
model="openrouter/unmapped-model",
raw_response=httpx.Response(
200,
json={
"id": "resp_test",
"created_at": 1,
"model": "unmapped-model",
"object": "response",
"output": [],
"status": "completed",
"usage": usage,
},
),
logging_obj=MagicMock(),
)
assert get_response_cost_from_hidden_params(response.hidden_params) == cost
@pytest.mark.parametrize(
"event_type",
["response.completed", "response.incomplete", "response.failed"],
)
def test_openrouter_terminal_stream_events_return_provider_reported_cost(event_type):
event = OpenRouterResponsesAPIConfig().transform_streaming_response(
model="openrouter/unmapped-model",
parsed_chunk={
"type": event_type,
"response": {
"id": "resp_test",
"created_at": 1,
"model": "unmapped-model",
"object": "response",
"output": [],
"status": "completed",
"usage": {
"input_tokens": 2,
"output_tokens": 3,
"total_tokens": 5,
"cost": 1.23e-5,
},
},
},
logging_obj=MagicMock(),
)
assert isinstance(
event,
(ResponseCompletedEvent, ResponseIncompleteEvent, ResponseFailedEvent),
)
assert event.response.hidden_params["additional_headers"][RESPONSE_COST_HEADER] == 1.23e-5

View file

@ -0,0 +1,256 @@
import json
from collections.abc import Mapping
from typing import Final
import httpx
import pytest
from botocore.credentials import Credentials
from pydantic import TypeAdapter
from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.llms.base_llm.decisions.transformation import systemone_request_to_ir
from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM
from litellm.llms.strands_decider.decisions.transformation import (
AGENTCORE_SESSION_HEADER,
StrandsDeciderDecisionsConfig,
)
from litellm.types.decisions import DecisionsIRResponse, DecisionsRequestBody
from litellm.types.llms.bedrock import AwsAuthParams
_ARN: Final = "arn:aws:bedrock-agentcore:us-west-2:123456789012:runtime/qa_decider-AbC123xyz0"
_BODY: Final[Mapping[str, object]] = {
"model": "strands-decider-2B-hobson-v21",
"state": "My order arrived broken.",
"questions": {"is_complaint": {"type": "noul", "instructions": "Is this a complaint?"}},
}
_IR_REQUEST: Final = systemone_request_to_ir(
TypeAdapter(DecisionsRequestBody).validate_python({"state": _BODY["state"], "questions": _BODY["questions"]})
)
class _RecordingAWS(BaseAWSLLM):
def __init__(self) -> None:
super().__init__()
self.resolved: tuple[tuple[AwsAuthParams, str | None], ...] = ()
def resolve_credentials(self, auth_params: AwsAuthParams, aws_region_name: str | None) -> Credentials:
self.resolved = (*self.resolved, (auth_params, aws_region_name))
return Credentials(access_key="AKIDEXAMPLE", secret_key="example-secret")
def _sign(
config: StrandsDeciderDecisionsConfig,
api_base: str,
headers: Mapping[str, str],
api_key: str | None = None,
litellm_params: Mapping[str, object] | None = None,
) -> tuple[Mapping[str, str], bytes | None]:
return config.sign_request(
headers=headers,
url=config.get_complete_url(api_base=api_base, model="strands-decider-2B-hobson-v21"),
api_base=api_base,
body=_BODY,
api_key=api_key,
litellm_params=litellm_params or {},
)
@pytest.mark.parametrize(
("arn", "expected_url"),
[
(
_ARN,
"https://bedrock-agentcore.us-west-2.amazonaws.com/runtimes/"
"arn%3Aaws%3Abedrock-agentcore%3Aus-west-2%3A123456789012%3Aruntime%2Fqa_decider-AbC123xyz0/invocations",
),
(
"arn:aws-cn:bedrock-agentcore:cn-north-1:123456789012:runtime/qa_decider-AbC123xyz0",
"https://bedrock-agentcore.cn-north-1.amazonaws.com.cn/runtimes/"
"arn%3Aaws-cn%3Abedrock-agentcore%3Acn-north-1%3A123456789012%3Aruntime%2Fqa_decider-AbC123xyz0"
"/invocations",
),
(
"arn:aws-us-gov:bedrock-agentcore:us-gov-west-1:123456789012:runtime/qa_decider-AbC123xyz0",
"https://bedrock-agentcore.us-gov-west-1.amazonaws.com/runtimes/"
"arn%3Aaws-us-gov%3Abedrock-agentcore%3Aus-gov-west-1%3A123456789012%3Aruntime%2Fqa_decider-AbC123xyz0"
"/invocations",
),
],
)
def test_runtime_arn_targets_the_regional_invoke_agent_runtime_url(arn: str, expected_url: str) -> None:
assert StrandsDeciderDecisionsConfig().get_complete_url(api_base=arn, model="any") == expected_url
@pytest.mark.parametrize(
"api_base",
[
"https://strands.example",
"arn:aws:bedrock-agentcore:us-west-2:123456789012:runtime/qa_decider-AbC123xyz0/runtime-endpoint/DEFAULT",
"arn:aws:bedrock-agentcore:evil.example/x:123456789012:runtime/qa_decider-AbC123xyz0",
"arn:aws:bedrock:us-west-2:123456789012:runtime/qa_decider-AbC123xyz0",
],
)
def test_other_api_bases_keep_the_plain_systemone_request(api_base: str) -> None:
aws: Final = _RecordingAWS()
config: Final = StrandsDeciderDecisionsConfig(aws=aws)
headers: Final = {"Content-Type": "application/json"}
assert config.get_complete_url(api_base=api_base, model="any") == f"{api_base}/v1/systemone"
assert _sign(config, api_base, headers) == (headers, None)
assert aws.resolved == ()
def test_sign_request_signs_the_body_for_the_arn_region_with_deployment_credentials() -> None:
aws: Final = _RecordingAWS()
config: Final = StrandsDeciderDecisionsConfig(aws=aws)
signed_headers, signed_body = _sign(
config,
_ARN,
{"Content-Type": "application/json"},
litellm_params={"aws_profile_name": "qa", "aws_role_name": "decider-role", "aws_region_name": "us-east-1"},
)
assert signed_body == json.dumps(_BODY).encode()
assert len(aws.resolved) == 1
auth_params, region = aws.resolved[0]
assert (auth_params.aws_profile_name, auth_params.aws_role_name, region) == ("qa", "decider-role", "us-west-2")
authorization: Final = signed_headers["Authorization"]
assert authorization.startswith("AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/")
assert "/us-west-2/bedrock-agentcore/aws4_request" in authorization
assert AGENTCORE_SESSION_HEADER.lower() in authorization
def test_default_session_id_is_stable_per_runtime_and_within_the_agentcore_length_bounds() -> None:
other_arn: Final = _ARN.replace("AbC123xyz0", "ZyX987cba1")
first, _ = _sign(StrandsDeciderDecisionsConfig(aws=_RecordingAWS()), _ARN, {})
again, _ = _sign(StrandsDeciderDecisionsConfig(aws=_RecordingAWS()), _ARN, {})
other, _ = _sign(StrandsDeciderDecisionsConfig(aws=_RecordingAWS()), other_arn, {})
session_id: Final = first[AGENTCORE_SESSION_HEADER]
assert session_id == again[AGENTCORE_SESSION_HEADER]
assert session_id != other[AGENTCORE_SESSION_HEADER]
assert 33 <= len(session_id) <= 256
_CALLER_RESERVED_HEADERS: Final = {
AGENTCORE_SESSION_HEADER.lower(): "caller-session-0123456789abcdef0123456789",
"X-Amzn-Bedrock-AgentCore-Runtime-User-Id": "someone-else",
"X-Amz-Security-Token": "caller-token",
"Host": "evil.example",
}
@pytest.mark.parametrize("api_key", [None, "runtime-jwt"])
def test_caller_reserved_agentcore_headers_never_reach_the_runtime(api_key: str | None) -> None:
signed_headers, _ = _sign(
StrandsDeciderDecisionsConfig(aws=_RecordingAWS()),
_ARN,
{**_CALLER_RESERVED_HEADERS, "X-Request-Source": "batch"},
api_key=api_key,
)
default_headers, _ = _sign(StrandsDeciderDecisionsConfig(aws=_RecordingAWS()), _ARN, {}, api_key=api_key)
forwarded: Final = {name.lower(): value for name, value in signed_headers.items()}
assert forwarded[AGENTCORE_SESSION_HEADER.lower()] == default_headers[AGENTCORE_SESSION_HEADER]
assert not any(value in forwarded.values() for value in _CALLER_RESERVED_HEADERS.values())
assert "x-amzn-bedrock-agentcore-runtime-user-id" not in forwarded
assert "host" not in forwarded
assert forwarded["x-request-source"] == "batch"
def test_deployment_session_param_picks_the_session_over_the_default_and_any_caller_header() -> None:
replica_session: Final = "strands-decider-replica-a-0000000000"
signed_headers, _ = _sign(
StrandsDeciderDecisionsConfig(aws=_RecordingAWS()),
_ARN,
_CALLER_RESERVED_HEADERS,
litellm_params={"agentcore_runtime_session_id": replica_session},
)
session_headers: Final = [
value for name, value in signed_headers.items() if name.lower() == "x-amzn-bedrock-agentcore-runtime-session-id"
]
assert session_headers == [replica_session]
assert AGENTCORE_SESSION_HEADER.lower() in signed_headers["Authorization"]
def test_api_key_on_a_runtime_arn_sends_the_bearer_token_without_sigv4() -> None:
aws: Final = _RecordingAWS()
signed_headers, signed_body = _sign(
StrandsDeciderDecisionsConfig(aws=aws), _ARN, {"Authorization": "Bearer runtime-jwt"}, api_key="runtime-jwt"
)
assert signed_body is None
assert signed_headers["Authorization"] == "Bearer runtime-jwt"
assert AGENTCORE_SESSION_HEADER in signed_headers
assert aws.resolved == ()
def _response(url: str, payload: object) -> httpx.Response:
return httpx.Response(200, json=payload, request=httpx.Request("POST", url))
def _transform(api_base: str, payload: object) -> DecisionsIRResponse:
config: Final = StrandsDeciderDecisionsConfig()
url: Final = config.get_complete_url(api_base=api_base, model="strands-decider-2B-hobson-v21")
return config.transform_decisions_response(
model="strands-decider-2B-hobson-v21",
custom_llm_provider="strands_decider",
raw_response=_response(url, payload),
request=_IR_REQUEST,
)
@pytest.mark.parametrize(
("code", "status_code"),
[("bad_request", 400), ("invalid_request", 400), ("loading", 503), ("inference_error", 500), ("new_code", 500)],
)
def test_runtime_error_envelope_raises_with_the_mapped_status(code: str, status_code: int) -> None:
with pytest.raises(BaseLLMException) as raised:
_transform(_ARN, {"error": {"code": code, "message": "at most 16 questions"}})
assert raised.value.status_code == status_code
assert code in raised.value.message
assert "at most 16 questions" in raised.value.message
@pytest.mark.parametrize("code", ["bad_request", "loading", "inference_error"])
def test_plain_api_base_error_envelope_keeps_the_unexpected_response_error(code: str) -> None:
with pytest.raises(BaseLLMException) as raised:
_transform("https://strands.example", {"error": {"code": code, "message": "at most 16 questions"}})
assert raised.value.status_code == 500
assert raised.value.message.startswith("Decisions provider 'strands_decider' returned an unexpected response")
@pytest.mark.parametrize(
"url",
[
"https://strands.example/runtimes/arn%3Aaws%3Abedrock-agentcore%3Aus-west-2%3A123456789012%3Aruntime"
"%2Fqa_decider-AbC123xyz0/invocations",
"https://bedrock-agentcore.us-west-2.amazonaws.com/runtimes/not-an-arn/invocations",
],
)
def test_error_envelope_from_a_url_that_is_not_the_runtime_keeps_the_unexpected_response_error(url: str) -> None:
with pytest.raises(BaseLLMException) as raised:
StrandsDeciderDecisionsConfig().transform_decisions_response(
model="strands-decider-2B-hobson-v21",
custom_llm_provider="strands_decider",
raw_response=_response(url, {"error": {"code": "bad_request", "message": "at most 16 questions"}}),
request=_IR_REQUEST,
)
assert raised.value.status_code == 500
@pytest.mark.parametrize("payload", [["not", "an", "object"], {"error": "unstructured"}])
def test_runtime_reply_that_is_not_an_error_envelope_keeps_the_unexpected_response_error(payload: object) -> None:
with pytest.raises(BaseLLMException) as raised:
_transform(_ARN, payload)
assert raised.value.status_code == 500
assert raised.value.message.startswith("Decisions provider 'strands_decider' returned an unexpected response")

View file

@ -2854,6 +2854,22 @@ class TestIsRequestBodySafeBlocksBedrockProjectOverride:
)
def test_agentcore_runtime_session_id_in_request_body_is_rejected_without_admin_opt_in():
body: Final = {"model": "strands-decider", "agentcore_runtime_session_id": "caller-session-0123456789abcdef0123"}
with pytest.raises(ValueError, match="agentcore_runtime_session_id"):
is_request_body_safe(request_body=body, general_settings={}, llm_router=None, model="strands-decider")
assert (
is_request_body_safe(
request_body=body,
general_settings={"allow_client_side_credentials": True},
llm_router=None,
model="strands-decider",
)
is True
)
class TestIsRequestBodySafeBlocksClaudePlatformWorkspaceOverride:
@pytest.mark.parametrize(
"alias", ["workspace_id", "aws_workspace_id", "anthropic_workspace_id", "anthropic-workspace-id"]

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

@ -203,9 +203,7 @@ def clean_agent_registry():
@pytest.mark.asyncio
async def test_get_agent_with_read_through_recovers_agent_created_on_sibling_replica(
clean_agent_registry, monkeypatch
):
async def test_get_agent_with_read_through_recovers_agent_created_on_sibling_replica(clean_agent_registry, monkeypatch):
from unittest.mock import AsyncMock, MagicMock
import litellm.proxy.proxy_server as proxy_server
@ -601,7 +599,9 @@ async def test_resync_guardrails_syncs_decrypted_litellm_params(monkeypatch):
synced: list[dict] = []
monkeypatch.setattr(proxy_server, "prisma_client", prisma_client)
monkeypatch.setattr(proxy_server, "store_model_in_db", True)
monkeypatch.setattr(IN_MEMORY_GUARDRAIL_HANDLER, "sync_guardrail_from_db", lambda guardrail: synced.append(guardrail))
monkeypatch.setattr(
IN_MEMORY_GUARDRAIL_HANDLER, "sync_guardrail_from_db", lambda guardrail: synced.append(guardrail)
)
monkeypatch.setattr(read_through_module, "_initialized_guardrail", lambda guardrail_name: MagicMock())
assert await _resync_guardrails("enc-guardrail") is True
@ -610,15 +610,21 @@ async def test_resync_guardrails_syncs_decrypted_litellm_params(monkeypatch):
@pytest.mark.asyncio
@pytest.mark.parametrize("lookup", ["agent-id", "Agent name"])
async def test_agent_read_through_hydrates_identity_binding(lookup, clean_agent_registry, fresh_agent_read_through, monkeypatch):
async def test_agent_read_through_hydrates_identity_binding(
lookup, clean_agent_registry, fresh_agent_read_through, monkeypatch
):
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock
from litellm.proxy.common_utils.registry_read_through import get_agent_with_read_through
binding = {
"agent_id": "agent-id", "provider": "microsoft_entra", "tenant_id": "tenant", "client_id": "client",
"issuer": "https://login.microsoftonline.com/tenant/v2.0", "revision": "revision",
"agent_id": "agent-id",
"provider": "microsoft_entra",
"tenant_id": "tenant",
"client_id": "client",
"issuer": "https://login.microsoftonline.com/tenant/v2.0",
"revision": "revision",
}
async def load_row(*, where, include):
@ -733,3 +739,49 @@ async def test_agent_read_through_answers_a_loaded_agent_without_reading_the_db(
assert await agent_registry_read_through.attempt("wired-loaded-agent-id") is True
assert await agent_registry_read_through.attempt("wired-loaded-agent") is True
@pytest.mark.asyncio
@pytest.mark.parametrize("router_holds_the_name_after_reconcile", [True, False])
async def test_resync_model_deployments_reconciles_every_db_row_when_the_missed_model_is_an_auto_router(
monkeypatch: pytest.MonkeyPatch, router_holds_the_name_after_reconcile: bool
) -> None:
from unittest.mock import AsyncMock, MagicMock
from litellm.proxy import proxy_server
from litellm.proxy.common_utils.encrypt_decrypt_utils import encrypt_value_helper
from litellm.proxy.common_utils.registry_read_through import _resync_model_deployments
monkeypatch.setenv("LITELLM_SALT_KEY", "sk-salt-auto-router-read-through")
router_name: Final = "auto-router-created-on-a-sibling-replica"
row: Final = MagicMock()
row.model_name = router_name
row.litellm_params = {
"model": encrypt_value_helper("auto_router/complexity-router"),
"auto_router_config": {"model_tiers": {"SIMPLE": "simple-tier", "COMPLEX": "complex-tier"}},
}
prisma_client: Final = MagicMock()
prisma_client.db.litellm_proxymodeltable.find_many = AsyncMock(return_value=[row])
router: Final = MagicMock()
router.model_names = []
router.has_model_id.return_value = False
reconciled: Final = MagicMock()
async def reconcile_every_row(prisma_client: object, proxy_logging_obj: object) -> None:
reconciled(prisma_client=prisma_client, proxy_logging_obj=proxy_logging_obj)
router.model_names = (
[router_name, "simple-tier", "complex-tier"] if router_holds_the_name_after_reconcile else []
)
def reject_single_row_path(db_models: object) -> None:
raise AssertionError(f"an auto router row took the single-row path: {db_models}")
monkeypatch.setattr(proxy_server, "prisma_client", prisma_client)
monkeypatch.setattr(proxy_server, "store_model_in_db", True)
monkeypatch.setattr(proxy_server, "llm_router", router)
monkeypatch.setattr(proxy_server, "llm_model_list", None)
monkeypatch.setattr(proxy_server.proxy_config, "add_deployment", reconcile_every_row)
monkeypatch.setattr(proxy_server.proxy_config, "_add_deployment", reject_single_row_path)
assert await _resync_model_deployments(router_name) is router_holds_the_name_after_reconcile
reconciled.assert_called_once_with(prisma_client=prisma_client, proxy_logging_obj=proxy_server.proxy_logging_obj)

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
@ -257,6 +259,51 @@ def test_proxy_decisions_dispatches_strands_decider(
assert "authorization" not in upstream.calls[0].request.headers
def test_proxy_systemone_dispatches_databricks_serving_endpoint(
client: TestClient,
monkeypatch: pytest.MonkeyPatch,
respx_mock: respx.MockRouter,
) -> None:
monkeypatch.delenv("DATABRICKS_API_BASE", raising=False)
monkeypatch.delenv("DATABRICKS_API_KEY", raising=False)
monkeypatch.delenv("DATABRICKS_TOKEN", raising=False)
router: Final = litellm.Router(
model_list=[
{
"model_name": "databricks-decider",
"litellm_params": {
"model": "databricks/databricks-openjev-qwen35-4b",
"api_base": "https://workspace.example/serving-endpoints",
"api_key": "dapi-deployment",
},
}
]
)
monkeypatch.setattr(litellm.proxy.proxy_server, "llm_router", router)
upstream: Final = respx_mock.post(
"https://workspace.example/serving-endpoints/databricks-openjev-qwen35-4b/invocations"
).respond(json={**_RESPONSE, "model": "/mosaicml/local_model"})
response: Final = client.post(
"/v1/systemone",
json={
"model": "databricks-decider",
"state": {"source": "proxy-test"},
"questions": {"is_defect": {"type": "noul", "instructions": "Is this a defect?"}},
},
)
assert response.status_code == 200, response.text
assert response.json()["answers"] == _RESPONSE["answers"]
assert upstream.called
assert json.loads(upstream.calls[0].request.content) == {
"model": "databricks-openjev-qwen35-4b",
"state": {"source": "proxy-test"},
"questions": {"is_defect": {"type": "noul", "instructions": "Is this a defect?"}},
}
assert upstream.calls[0].request.headers["authorization"] == "Bearer dapi-deployment"
def test_proxy_decisions_without_model_uses_the_proxy_default_model(
client: TestClient,
monkeypatch: pytest.MonkeyPatch,
@ -530,6 +577,68 @@ def test_openai_format_decisions_reach_an_openai_deployment_unchanged_including_
assert float(response.headers["x-litellm-response-cost"]) == pytest.approx(expected_cost)
@pytest.mark.parametrize("safety_identifier", (7, ["end-user-1"]), ids=("numeric", "list"))
@pytest.mark.parametrize("deployment_drops_params", (False, True), ids=("strict", "drop_params"))
def test_a_non_string_safety_identifier_is_refused_unless_the_deployment_drops_params(
client: TestClient,
monkeypatch: pytest.MonkeyPatch,
respx_mock: respx.MockRouter,
safety_identifier: object,
deployment_drops_params: bool,
) -> None:
monkeypatch.setattr(litellm, "drop_params", False)
monkeypatch.setattr(litellm, "api_base", None)
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
monkeypatch.delenv("OPENAI_API_BASE", raising=False)
monkeypatch.setattr(
litellm.proxy.proxy_server,
"llm_router",
litellm.Router(
model_list=[
{
"model_name": "decider",
"litellm_params": {
"model": "openai/gpt-6-luna",
"api_key": "k",
"drop_params": deployment_drops_params,
},
}
]
),
)
upstream: Final = respx_mock.post("https://api.openai.com/v1/decisions").respond(
json={
"model": "gpt-6-luna",
"answers": _OPENAI_FORMAT_ANSWERS[:1],
"usage": {
"input_tokens": _INPUT_TOKENS,
"output_tokens": _OUTPUT_TOKENS,
"total_tokens": _INPUT_TOKENS + _OUTPUT_TOKENS,
},
}
)
request_body: Final = {
"model": "decider",
"input": "The package arrived with a broken screen.",
"questions": _OPENAI_FORMAT_REQUEST["questions"][:1],
"safety_identifier": safety_identifier,
}
response: Final = client.post("/v1/decisions", json=request_body)
if not deployment_drops_params:
assert response.status_code == 400, response.text
assert "safety_identifier" in response.json()["error"]["message"]
assert not upstream.called
return
assert response.status_code == 200, response.text
assert json.loads(upstream.calls[0].request.content) == {
"model": "gpt-6-luna",
"input": "The package arrived with a broken screen.",
"questions": _OPENAI_FORMAT_REQUEST["questions"][:1],
}
def _decisions_feature() -> LazyFeature:
return next(feature for feature in LAZY_FEATURES if feature.name == "decisions")
@ -581,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

@ -414,7 +414,7 @@ def test_initialize_rejects_invalid_streaming_scan_settings(mock_mgr, overrides,
def test_add_guardrail_settings_returns_decisions_providers():
import asyncio
from litellm.decisions.main import supported_decisions_providers
from litellm.decisions.call import supported_decisions_providers
from litellm.proxy.guardrails.guardrail_endpoints import get_guardrail_ui_settings
settings: Final = asyncio.run(get_guardrail_ui_settings())

View file

@ -146,6 +146,8 @@ def test_tier_config_is_normalized_and_unknown_router_extras_are_rejected() -> N
({"provider": "laya", "model": "english", "api_key": "sk-member"}, "api_key"),
({"provider": "bespoke", "model": "nimble-latest", "api_base": "https://collector.invalid"}, "api_base"),
({"provider": "bespoke", "model": "nimble-latest", "api_key": "sk-member"}, "api_key"),
({"provider": "databricks", "model": "my-openjev", "api_base": "https://collector.invalid"}, "opensource_classifier_config"),
({"provider": "databricks", "model": "my-openjev", "api_key": "dapi-member"}, "api_key"),
],
)
@pytest.mark.parametrize("legacy", [False, True])
@ -164,7 +166,10 @@ def test_members_cannot_move_the_jev_classifier_off_the_proxys_typesafe_account(
assert denied.value.detail == f"Invalid member auto-router configuration at {rejected_at}."
@pytest.mark.parametrize(("provider", "model"), [("typesafe", "jev-preview"), ("laya", "english"), ("bespoke", "nimble-latest")])
@pytest.mark.parametrize(
("provider", "model"),
[("typesafe", "jev-preview"), ("laya", "english"), ("bespoke", "nimble-latest"), ("databricks", "my-openjev")],
)
@pytest.mark.parametrize("legacy", [False, True])
def test_members_can_still_tune_the_jev_classifier(provider: str, model: str, legacy: bool) -> None:
validated: Final = validate_member_auto_router_config(

View file

@ -14,7 +14,7 @@ from litellm.proxy.pass_through_endpoints.success_handler import PassThroughEndp
@pytest.fixture(autouse=True)
def local_model_cost_map(monkeypatch: pytest.MonkeyPatch):
def local_model_cost_map(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True")
monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url=""))
@ -222,14 +222,17 @@ async def test_oss_gateway_accounts_for_checkpoint_usage_and_registered_cost(
await budget_limiter.is_key_within_model_budget(auth, f"{provider}/{requested}")
def test_openrouter_decisions_response_is_priced_from_request_model_registry_row():
def test_openrouter_decisions_response_uses_provider_cost_without_cost_map_row(
monkeypatch: pytest.MonkeyPatch,
):
logging_obj = _logging_obj()
model_cost = litellm.model_cost["openrouter/typesafe/jev-1.13"]
cost: Final = 1.5834e-5
monkeypatch.delitem(litellm.model_cost, "openrouter/typesafe/jev-1.13", raising=False)
response = TypeSafePassthroughLoggingHandler.typesafe_passthrough_handler(
httpx_response=_response(),
response_body={
"model": "typesafe/jev-1.13-20260917",
"usage": {"input_tokens": 282, "output_tokens": 20},
"usage": {"input_tokens": 282, "output_tokens": 20, "cost": cost},
},
logging_obj=logging_obj,
url_route="https://openrouter.ai/api/alpha/decisions",
@ -241,10 +244,9 @@ def test_openrouter_decisions_response_is_priced_from_request_model_registry_row
custom_llm_provider="openrouter",
)
expected_cost = 282 * model_cost["input_cost_per_token"] + 20 * model_cost["output_cost_per_token"]
assert response["kwargs"]["model"] == "openrouter/typesafe/jev-1.13-20260917"
assert response["kwargs"]["custom_llm_provider"] == "openrouter"
assert response["kwargs"]["response_cost"] == pytest.approx(expected_cost)
assert response["kwargs"]["response_cost"] == cost
assert response["kwargs"]["combined_usage_object"].prompt_tokens == 282
assert response["kwargs"]["combined_usage_object"].completion_tokens == 20
assert response["kwargs"]["combined_usage_object"].total_tokens == 302

View file

@ -451,7 +451,14 @@ def test_jev_config_requires_classifier_config() -> None:
)
@pytest.mark.parametrize(
("provider", "model", "canonical_provider"),
[(None, "jev-latest", "jev"), ("typesafe", "jev-latest", "jev"), ("jev", "jev-latest", "jev"), ("laya", "english", "laya"), ("bespoke", "nimble-latest", "bespoke")],
[
(None, "jev-latest", "jev"),
("typesafe", "jev-latest", "jev"),
("jev", "jev-latest", "jev"),
("laya", "english", "laya"),
("bespoke", "nimble-latest", "bespoke"),
("databricks", "databricks-openjev-qwen35-4b", "databricks"),
],
)
def test_classifier_aliases_load_and_serialize_one_canonical_config(
classifier_type: str, config_key: str, provider: str | None, model: str, canonical_provider: str
@ -533,6 +540,85 @@ async def test_oss_routes_with_its_own_credentials_and_accounts_the_checkpoint(
assert recorder.calls[0]["response_cost"] == pytest.approx(0.31)
def test_databricks_requires_the_bare_serving_endpoint_name_as_the_model() -> None:
with pytest.raises(ValueError, match="model is required for provider 'databricks'"):
JevClassifierConfig.model_validate({"provider": "databricks"})
with pytest.raises(ValueError, match="bare endpoint name"):
JevClassifierConfig.model_validate({"provider": "databricks", "model": "serving-endpoints/my-openjev"})
with pytest.raises(ValueError, match="bare endpoint name"):
JevClassifierConfig.model_validate({"provider": "databricks", "model": ".."})
assert JevClassifierConfig.model_validate({"provider": "databricks", "model": "my-openjev"}).model == "my-openjev"
@pytest.mark.asyncio
@pytest.mark.parametrize("configured_key", [False, True])
@pytest.mark.parametrize("legacy", [False, True])
async def test_databricks_routes_through_the_serving_endpoint_and_accounts_the_endpoint_model(
monkeypatch: pytest.MonkeyPatch, configured_key: bool, legacy: bool
) -> None:
endpoint: Final = "databricks-openjev-qwen35-4b"
monkeypatch.setenv("TYPESAFE_API_KEY", "never-send-typesafe-key")
monkeypatch.setenv("DATABRICKS_API_BASE", "https://workspace.test/serving-endpoints")
monkeypatch.delenv("DATABRICKS_API_KEY", raising=False)
monkeypatch.setenv("DATABRICKS_TOKEN", "dapi-env-token")
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
monkeypatch.setitem(litellm.model_cost, f"databricks/{endpoint}", {"input_cost_per_token": 0.01})
recorder: Final = _UsageRecorder(f"databricks/{endpoint}")
monkeypatch.setattr(litellm, "_async_success_callback", [recorder])
router: Final = ComplexityRouter(
"databricks-route",
litellm.Router(model_list=[]),
{
"classifier_type": "jev" if legacy else "oss_classifier",
"jev_classifier_config" if legacy else "opensource_classifier_config": {
"provider": "databricks",
"model": endpoint,
**({"api_key": "dapi-configured", "api_base": "https://workspace.test/serving-endpoints"} if configured_key else {}),
},
"tiers": {"SIMPLE": "cheap"},
},
derive_savings_baseline=False,
)
with respx.mock(assert_all_called=True) as upstream:
route: Final = upstream.post(f"https://workspace.test/serving-endpoints/{endpoint}/invocations").respond(
200,
json={
"model": "/mosaicml/local_model",
"answers": {"tier": _answer().model_dump()},
"usage": {"input_tokens": 31, "output_tokens": 0},
},
)
outcome: Final = await router.aclassify("choose a tier")
await GLOBAL_LOGGING_WORKER.flush()
assert outcome.cause == "jev_classifier"
assert outcome.jev_verdict is not None
assert (outcome.jev_verdict.provider, outcome.jev_verdict.model) == ("databricks", endpoint)
assert outcome.classifier_cost == pytest.approx(0.31)
sent: Final = route.calls.last.request
assert sent.headers.get("authorization") == ("Bearer dapi-configured" if configured_key else "Bearer dapi-env-token")
assert json.loads(sent.content)["model"] == endpoint
assert len(recorder.calls) == 1
assert recorder.calls[0]["response_cost"] == pytest.approx(0.31)
def test_databricks_without_a_base_or_key_fails_at_client_build(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("DATABRICKS_API_BASE", raising=False)
monkeypatch.delenv("DATABRICKS_API_KEY", raising=False)
monkeypatch.delenv("DATABRICKS_TOKEN", raising=False)
with pytest.raises(ValueError, match="DATABRICKS_API_BASE"):
ComplexityRouter(
"databricks-route",
litellm.Router(model_list=[]),
{
"classifier_type": "oss_classifier",
"opensource_classifier_config": {"provider": "databricks", "model": "databricks-openjev-qwen35-4b"},
"tiers": {"SIMPLE": "cheap"},
},
derive_savings_baseline=False,
)
def test_jev_config_is_rejected_for_other_classifier_types() -> None:
with pytest.raises(ValueError, match="has no effect"):
ComplexityRouterConfig.model_validate(
@ -570,6 +656,22 @@ def test_jev_api_base_without_its_own_key_is_rejected_so_the_environment_key_sta
assert JevClassifierConfig(api_key="sk-own").api_base is None
def test_databricks_api_base_without_its_own_key_is_rejected_so_the_workspace_token_stays_home() -> None:
with pytest.raises(ValueError, match="DATABRICKS_API_KEY or DATABRICKS_TOKEN is only sent to DATABRICKS_API_BASE"):
JevClassifierConfig.model_validate(
{"provider": "databricks", "model": "my-openjev", "api_base": "https://collector.invalid/serving-endpoints"}
)
paired: Final = JevClassifierConfig.model_validate(
{
"provider": "databricks",
"model": "my-openjev",
"api_base": "https://workspace.invalid/serving-endpoints",
"api_key": "dapi-own",
}
)
assert (paired.api_base, paired.api_key) == ("https://workspace.invalid/serving-endpoints", "dapi-own")
@pytest.mark.parametrize(
("probabilities", "confidence"),
[

View file

View file

@ -0,0 +1,138 @@
from __future__ import annotations
import asyncio
import json
from collections.abc import Mapping
from types import MappingProxyType
from typing import Final
import pytest
import respx
from respx.models import Call
import litellm
from litellm.integrations.custom_logger import CustomLogger
from litellm.litellm_core_utils.logging_worker import GLOBAL_LOGGING_WORKER
from litellm.types.decisions import DecisionsResponse, OpenAIDecisionResponse
_STATE: Final = "Customer asked for a refund"
_SYSTEMONE_QUESTIONS: Final[Mapping[str, object]] = MappingProxyType(
{"is_refund": {"type": "noul", "instructions": "Is this a refund request?"}}
)
_OPENAI_QUESTIONS: Final[tuple[Mapping[str, object], ...]] = (
{"type": "predicate", "name": "is_refund", "instructions": "Is this a refund request?"},
)
_SYSTEMONE_RESPONSE: Final[Mapping[str, object]] = {
"model": "jev-1.13",
"answers": {"is_refund": {"type": "noul", "noul": 0.93}},
"usage": {"input_tokens": 12, "output_tokens": 1},
}
_TYPESAFE_URL: Final = "https://api.typesafe.ai/v1/systemone"
@pytest.fixture(autouse=True)
def _httpx_transport(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
litellm.in_memory_llm_clients_cache.flush_cache()
class _CallTypeRecorder(CustomLogger):
def __init__(self) -> None:
super().__init__()
self.call_types: list[str] = [] # mutable-ok: test recorder collects logger callbacks
async def async_log_success_event(self, kwargs, response_obj, start_time, end_time) -> None:
self.call_types.append(kwargs["call_type"])
@pytest.mark.asyncio
@pytest.mark.parametrize("use_async", (True, False), ids=("asystemone", "systemone"))
async def test_systemone_sends_the_systemone_wire_format_and_returns_a_systemone_response(
use_async: bool, respx_mock: respx.MockRouter
) -> None:
route: Final = respx_mock.post(_TYPESAFE_URL).respond(json=_SYSTEMONE_RESPONSE)
kwargs: Final[Mapping[str, object]] = MappingProxyType(
{"model": "typesafe/jev-1.13", "state": _STATE, "questions": _SYSTEMONE_QUESTIONS, "api_key": "caller-key"}
)
response: Final = await litellm.asystemone(**kwargs) if use_async else litellm.systemone(**kwargs)
assert route.called
call: Final[Call] = respx_mock.calls.last
assert json.loads(call.request.content) == {
"model": "jev-1.13",
"state": _STATE,
"questions": {"is_refund": {"type": "noul", "instructions": "Is this a refund request?"}},
}
assert isinstance(response, DecisionsResponse)
assert response.answers["is_refund"].model_dump(mode="json") == {"type": "noul", "noul": 0.93}
@pytest.mark.asyncio
async def test_systemone_rejects_openai_input_and_points_at_decisions(respx_mock: respx.MockRouter) -> None:
route: Final = respx_mock.post(_TYPESAFE_URL).respond(json=_SYSTEMONE_RESPONSE)
with pytest.raises(litellm.BadRequestError, match=r"litellm\.decisions") as caught:
await litellm.asystemone(
model="typesafe/jev-1.13", input=_STATE, questions=_SYSTEMONE_QUESTIONS, api_key="caller-key"
)
assert caught.value.status_code == 400
assert not route.called
@pytest.mark.asyncio
@pytest.mark.parametrize("use_async", (True, False), ids=("adecisions", "decisions"))
async def test_decisions_with_state_still_sends_the_systemone_format_and_warns(
use_async: bool, respx_mock: respx.MockRouter
) -> None:
route: Final = respx_mock.post(_TYPESAFE_URL).respond(json=_SYSTEMONE_RESPONSE)
kwargs: Final[Mapping[str, object]] = MappingProxyType(
{"model": "typesafe/jev-1.13", "state": _STATE, "questions": _SYSTEMONE_QUESTIONS, "api_key": "caller-key"}
)
with pytest.warns(UserWarning, match=r"litellm\.systemone"):
response: Final = await litellm.adecisions(**kwargs) if use_async else litellm.decisions(**kwargs)
assert route.called
call: Final[Call] = respx_mock.calls.last
assert json.loads(call.request.content) == {
"model": "jev-1.13",
"state": _STATE,
"questions": {"is_refund": {"type": "noul", "instructions": "Is this a refund request?"}},
}
assert isinstance(response, DecisionsResponse)
assert response.answers["is_refund"].model_dump(mode="json") == {"type": "noul", "noul": 0.93}
@pytest.mark.asyncio
async def test_systemone_rejects_openai_question_lists_before_http(respx_mock: respx.MockRouter) -> None:
route: Final = respx_mock.post(_TYPESAFE_URL).respond(json=_SYSTEMONE_RESPONSE)
with pytest.raises(litellm.BadRequestError, match="Invalid Decisions request"):
await litellm.asystemone(
model="typesafe/jev-1.13", state=_STATE, questions=_OPENAI_QUESTIONS, api_key="caller-key"
)
assert not route.called
@pytest.mark.asyncio
async def test_router_asystemone_logs_the_systemone_call_type(
respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
) -> None:
respx_mock.post(_TYPESAFE_URL).respond(json=_SYSTEMONE_RESPONSE)
recorder: Final = _CallTypeRecorder()
monkeypatch.setattr(litellm, "callbacks", [recorder])
router: Final = litellm.Router(
model_list=[{"model_name": "jev", "litellm_params": {"model": "typesafe/jev-1.13", "api_key": "caller-key"}}]
)
response: Final = await router.asystemone(model="jev", state=_STATE, questions=_SYSTEMONE_QUESTIONS)
await asyncio.sleep(0)
GLOBAL_LOGGING_WORKER.start()
await asyncio.wait_for(GLOBAL_LOGGING_WORKER.flush(), timeout=10.0)
assert isinstance(response, DecisionsResponse)
assert recorder.call_types == ["asystemone"]
assert not isinstance(response, OpenAIDecisionResponse)

View file

@ -22,15 +22,7 @@ vi.mock("./detailNavigation", () => ({
}));
vi.mock("@/components/molecules/DecisionModelsBanner", () => ({
default: ({ onAddModel }: { onAddModel?: () => void }) => (
<section aria-label="Decision models">
{onAddModel && (
<button type="button" onClick={onAddModel}>
Add a decision model
</button>
)}
</section>
),
default: () => <section aria-label="Decision models" />,
}));
vi.mock("@/components/model_info_view", () => ({
default: ({ modelId }: { modelId: string }) => <div data-testid="model-info">model:{modelId}</div>,
@ -107,21 +99,6 @@ describe("ModelsAndEndpointsPage", () => {
expect(screen.queryByTestId("panel-all-models")).not.toBeInTheDocument();
});
it("opens the Add Model tab from the decision models banner", async () => {
const user = userEvent.setup();
renderPage();
await user.click(screen.getByRole("button", { name: "Add a decision model" }));
expect(screen.getByTestId("panel-add")).toBeInTheDocument();
expect(screen.queryByTestId("panel-all-models")).not.toBeInTheDocument();
});
it("does not offer the banner's Add a decision model to a session that cannot add models", () => {
mockUseAuthorized.mockReturnValue(NON_ADMIN);
renderPage();
expect(screen.getByRole("region", { name: "Decision models" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Add a decision model" })).not.toBeInTheDocument();
});
it("does not show a view-only session the decision models banner, whose Playground it cannot open", () => {
mockUseAuthorized.mockReturnValue(VIEW_ONLY_ADMIN);
renderPage();

View file

@ -177,9 +177,7 @@ export default function ModelsAndEndpointsPage() {
</div>
</div>
{!isViewOnly && (
<DecisionModelsBanner onAddModel={canCreate && !modelId ? () => setActiveKey("add") : undefined} />
)}
{!isViewOnly && <DecisionModelsBanner />}
{modelId ? (
<ModelInfoView

View file

@ -563,14 +563,6 @@ describe("AddModelPanel decision models", () => {
const decisionNotice = () => screen.queryByRole("note", { name: "Decision model notice" });
it("finds a provider by the name of one of its decision models", async () => {
const { user } = await setup();
await user.type(screen.getByRole("combobox", { name: /provider/i }), "jev");
expect(await screen.findByRole("option", { name: /TypeSafe/ })).toBeInTheDocument();
expect(screen.queryByRole("option", { name: /Anthropic/ })).not.toBeInTheDocument();
});
it("points a decision-only provider at the decision routes, the docs, and the playground", async () => {
const { user } = await setup();
await user.type(screen.getByRole("combobox", { name: /provider/i }), "TypeSafe");
@ -582,7 +574,7 @@ describe("AddModelPanel decision models", () => {
"href",
"https://docs.litellm.ai/docs/decisions",
);
expect(within(note).getByRole("link", { name: "test it in the System One playground" })).toHaveAttribute(
expect(within(note).getByRole("link", { name: "test it in the Decisions playground" })).toHaveAttribute(
"href",
"/ui/playground?tab=system-one",
);

View file

@ -0,0 +1,43 @@
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/cva.config";
import { Plus, Trash2 } from "lucide-react";
import { useId, type ReactNode } from "react";
import { IssueList, ValidationStatus } from "./JsonEditor";
import type { PayloadValidation } from "./lib/validatePayload";
export function RemoveButton({ label, onClick }: { label: string; onClick: () => void }) {
return (
<Button variant="ghost" size="icon" aria-label={label} title={label} onClick={onClick}>
<Trash2 />
</Button>
);
}
export function AddButton({ children, onClick }: { children: ReactNode; onClick: () => void }) {
return (
<Button variant="outline" size="sm" className="w-fit" onClick={onClick}>
<Plus />
{children}
</Button>
);
}
export function FormFrame({ validation, children }: { validation: PayloadValidation<unknown>; children: ReactNode }) {
const issuesId = useId();
return (
<div
className={cn(
"flex min-h-96 flex-1 flex-col overflow-hidden rounded-md border bg-background",
!validation.isValid && "border-destructive/60",
)}
>
<div className="flex items-center gap-2 border-b px-3 py-2">
<span className="text-sm font-medium">Request form</span>
<ValidationStatus validation={validation} />
</div>
<div className="grid min-h-80 flex-1 content-start gap-4 overflow-auto p-3">{children}</div>
<IssueList id={issuesId} validation={validation} />
</div>
);
}

View file

@ -1,12 +1,13 @@
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/cva.config";
import { CircleAlert, CircleCheck, TriangleAlert } from "lucide-react";
import { useId, useMemo, useRef } from "react";
import { useTheme } from "next-themes";
import { useId, useRef } from "react";
import { createElement, PrismLight as SyntaxHighlighter } from "react-syntax-highlighter";
import type { SyntaxHighlighterProps } from "react-syntax-highlighter";
import json from "react-syntax-highlighter/dist/esm/languages/prism/json";
import { findRootBlocks, ROOT_BLOCK_STYLES, type RootBlock } from "./lib/rootBlocks";
import type { SystemOnePayloadValidation } from "./lib/validatePayload";
import { vs, vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism";
import type { PayloadValidation } from "./lib/validatePayload";
SyntaxHighlighter.registerLanguage("json", json);
@ -14,24 +15,54 @@ const EDITOR_TEXT = "m-0 whitespace-pre-wrap wrap-anywhere py-3 font-mono text-x
const GUTTER_WIDTH = "w-11";
type LineRendererProps = Parameters<NonNullable<SyntaxHighlighterProps["renderer"]>>[0];
const CONTENT_INSET = "pl-14 pr-3";
const CODE_TAG_PROPS = { className: "language-json", style: { whiteSpace: "pre-wrap" } } as const;
type SyntaxTheme = NonNullable<SyntaxHighlighterProps["style"]>;
const THEME_BLOCKS = ['pre[class*="language-"]', 'code[class*="language-"]'] as const;
const TOKEN_COLORS = [
"[&_.token.property]:text-sky-700 dark:[&_.token.property]:text-sky-300",
"[&_.token.string]:text-emerald-700 dark:[&_.token.string]:text-emerald-300",
"[&_.token.number]:text-amber-700 dark:[&_.token.number]:text-amber-300",
"[&_.token.boolean]:text-violet-700 dark:[&_.token.boolean]:text-violet-300",
"[&_.token.null]:text-violet-700 dark:[&_.token.null]:text-violet-300",
"[&_.token.punctuation]:text-muted-foreground [&_.token.operator]:text-muted-foreground",
].join(" ");
function withoutBlockBackground(theme: SyntaxTheme): SyntaxTheme {
const stripped = THEME_BLOCKS.map((block) => {
const { background: _background, backgroundColor: _backgroundColor, ...rest } = theme[block] ?? {};
return [block, rest] as const;
});
return { ...theme, ...Object.fromEntries(stripped) };
}
const VS_CODE_DARK_PLUS = withoutBlockBackground(vscDarkPlus);
const VS_CODE_LIGHT_PLUS = {
...withoutBlockBackground(vs),
property: { color: "#0451a5" },
string: { color: "#a31515" },
number: { color: "#098658" },
boolean: { color: "#0000ff" },
punctuation: { color: "#000000" },
operator: { color: "#000000" },
} as const;
const INHERIT_TEXT = {
fontFamily: "inherit",
fontSize: "inherit",
lineHeight: "inherit",
whiteSpace: "pre-wrap",
overflowWrap: "anywhere",
textShadow: "none",
} as const;
const CODE_TAG_PROPS = { className: "language-json", style: INHERIT_TEXT } as const;
const TRANSPARENT_PRE = {
...INHERIT_TEXT,
background: "transparent",
border: 0,
margin: 0,
padding: 0,
overflow: "visible",
} as const;
interface JsonEditorProps {
value: string;
onChange: (value: string) => void;
validation: SystemOnePayloadValidation;
validation: PayloadValidation<unknown>;
label?: string;
placeholder?: string;
}
export function ValidationStatus({ validation }: { validation: SystemOnePayloadValidation }) {
export function ValidationStatus({ validation }: { validation: PayloadValidation<unknown> }) {
const errorCount = validation.issues.filter((issue) => issue.severity === "error").length;
if (errorCount > 0) {
return (
@ -43,7 +74,7 @@ export function ValidationStatus({ validation }: { validation: SystemOnePayloadV
return <Badge variant="secondary">Valid payload</Badge>;
}
export function IssueList({ id, validation }: { id: string; validation: SystemOnePayloadValidation }) {
export function IssueList({ id, validation }: { id: string; validation: PayloadValidation<unknown> }) {
if (validation.issues.length === 0) {
return (
<p id={id} role="status" className="flex items-center gap-1.5 border-t px-3 py-2 text-xs text-muted-foreground">
@ -69,39 +100,30 @@ export function IssueList({ id, validation }: { id: string; validation: SystemOn
);
}
function renderLines(rootBlocks: readonly RootBlock[]) {
return function LineRows({ rows, stylesheet, useInlineStyles }: LineRendererProps) {
return rows.map((row, line) => {
const lineElement = { node: row, stylesheet, useInlineStyles, key: line };
const block = rootBlocks.find(({ startLine, endLine }) => line >= startLine && line <= endLine);
return (
<div key={line} className="flex">
<span className={cn(GUTTER_WIDTH, "shrink-0 select-none pr-3 text-right text-muted-foreground")}>
{line + 1}
</span>
<span
className={cn(
"relative min-h-5 min-w-0 flex-1 px-3",
block && [
"before:absolute before:inset-y-0 before:left-0 before:w-0.5",
ROOT_BLOCK_STYLES[block.key].band,
line === block.startLine && "rounded-t-sm before:rounded-t-sm",
line === block.endLine && "rounded-b-sm before:rounded-b-sm",
],
)}
>
{createElement(lineElement)}
</span>
</div>
);
});
};
function LineRows({ rows, stylesheet, useInlineStyles }: LineRendererProps) {
return rows.map((row, line) => {
const lineElement = { node: row, stylesheet, useInlineStyles, key: line };
return (
<div key={line} className="flex">
<span className={cn(GUTTER_WIDTH, "shrink-0 select-none pr-3 text-right text-zinc-400 dark:text-zinc-500")}>
{line + 1}
</span>
<span className="min-h-5 min-w-0 flex-1 px-3">{createElement(lineElement)}</span>
</div>
);
});
}
export default function JsonEditor({ value, onChange, validation }: JsonEditorProps) {
export default function JsonEditor({
value,
onChange,
validation,
label = "System One JSON payload",
placeholder = "Paste or write a System One request",
}: JsonEditorProps) {
const issuesId = useId();
const highlightRef = useRef<HTMLDivElement>(null);
const renderer = useMemo(() => renderLines(findRootBlocks(value)), [value]);
const syntaxTheme = useTheme().resolvedTheme === "dark" ? VS_CODE_DARK_PLUS : VS_CODE_LIGHT_PLUS;
const lineCount = value.split("\n").length;
const hasErrors = !validation.isValid;
@ -121,26 +143,28 @@ export default function JsonEditor({ value, onChange, validation }: JsonEditorPr
{lineCount} {lineCount === 1 ? "line" : "lines"}
</span>
</div>
<div className="relative min-h-80 flex-1">
<div aria-hidden="true" className={cn(GUTTER_WIDTH, "absolute inset-y-0 left-0 border-r bg-muted/50")} />
<div className="relative min-h-80 flex-1 bg-background dark:bg-[#1e1e1e]">
<div
ref={highlightRef}
aria-hidden="true"
className={cn("absolute inset-0 overflow-hidden", EDITOR_TEXT, TOKEN_COLORS)}
>
className={cn(
GUTTER_WIDTH,
"absolute inset-y-0 left-0 border-r bg-muted/50 dark:border-zinc-700 dark:bg-zinc-800/60",
)}
/>
<div ref={highlightRef} aria-hidden="true" className={cn("absolute inset-0 overflow-hidden", EDITOR_TEXT)}>
<SyntaxHighlighter
language="json"
style={{}}
useInlineStyles={false}
style={syntaxTheme}
customStyle={TRANSPARENT_PRE}
PreTag="div"
codeTagProps={CODE_TAG_PROPS}
renderer={renderer}
renderer={LineRows}
>
{value}
</SyntaxHighlighter>
</div>
<textarea
aria-label="System One JSON payload"
aria-label={label}
aria-invalid={hasErrors}
aria-describedby={issuesId}
value={value}
@ -153,7 +177,7 @@ export default function JsonEditor({ value, onChange, validation }: JsonEditorPr
spellCheck={false}
autoCapitalize="off"
autoComplete="off"
placeholder="Paste or write a System One request"
placeholder={placeholder}
className={cn(
EDITOR_TEXT,
CONTENT_INSET,

View file

@ -0,0 +1,267 @@
"use client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { useId, useMemo } from "react";
import { AddButton, FormFrame, RemoveButton } from "./DecisionFormControls";
import { freeName, removeAt, replaceAt } from "./lib/formPayload";
import {
readDecisionsForm,
retypeDecisionQuestion,
type DecisionsFormPayload,
type DecisionsFormQuestion,
type DecisionsQuestionType,
} from "./lib/openAIDecisionsForm";
import type { OpenAIDecisionsRequest } from "./lib/openAIDecisions";
import type { PayloadValidation } from "./lib/validatePayload";
const QUESTION_TYPES: readonly DecisionsQuestionType[] = ["predicate", "choice", "score"];
const TYPE_LABELS = { predicate: "Predicate", choice: "Choice", score: "Score" };
interface QuestionEditorProps<Q extends DecisionsFormQuestion = DecisionsFormQuestion> {
question: Q;
onChange: (question: DecisionsFormQuestion) => void;
}
function Choices({ question, onChange }: QuestionEditorProps<Extract<DecisionsFormQuestion, { type: "choice" }>>) {
const choices = question.choices ?? [];
const write = (next: typeof choices) => onChange({ ...question, choices: next });
return (
<div className="grid gap-2">
<span className="text-sm font-medium">Choices</span>
{choices.map((choice, index) => (
<div key={index} className="grid gap-2 rounded-md border p-2">
<div className="flex items-center gap-2">
<Select
value={typeof choice.value === "string" ? "text" : String(choice.value)}
onValueChange={(type) => {
if (type === "text" || type === "true" || type === "false") {
const value = type === "text" ? String(choice.value) : type === "true";
write(replaceAt(choices, index, { ...choice, value }));
}
}}
>
<SelectTrigger className="w-40" aria-label={`Choice ${index + 1} value type`}>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="text">Text</SelectItem>
<SelectItem value="true">true (boolean)</SelectItem>
<SelectItem value="false">false (boolean)</SelectItem>
</SelectContent>
</Select>
{typeof choice.value === "string" && (
<Input
aria-label={`Choice ${index + 1} value`}
value={choice.value}
onChange={(event) => write(replaceAt(choices, index, { ...choice, value: event.target.value }))}
/>
)}
<RemoveButton label={`Remove choice ${index + 1}`} onClick={() => write(removeAt(choices, index))} />
</div>
<Input
aria-label={`Choice ${index + 1} description`}
value={choice.description ?? ""}
placeholder="Optional description"
onChange={(event) => write(replaceAt(choices, index, { ...choice, description: event.target.value }))}
/>
</div>
))}
<AddButton
onClick={() =>
write([
...choices,
{
value: freeName(
choices.map((c) => String(c.value)),
"option",
),
},
])
}
>
Add choice
</AddButton>
</div>
);
}
function Levels({ question, onChange }: QuestionEditorProps<Extract<DecisionsFormQuestion, { type: "score" }>>) {
const levels = question.levels ?? [];
const write = (next: typeof levels) => onChange({ ...question, levels: next });
return (
<div className="grid gap-2">
<span className="text-sm font-medium">Levels, lowest first</span>
{levels.map((level, index) => (
<div key={index} className="flex items-center gap-2">
<Input
aria-label={`Level ${index + 1} label`}
value={level.label}
placeholder="Label"
onChange={(event) => write(replaceAt(levels, index, { ...level, label: event.target.value }))}
/>
<Input
aria-label={`Level ${index + 1} description`}
value={level.description ?? ""}
placeholder="Optional description"
onChange={(event) => write(replaceAt(levels, index, { ...level, description: event.target.value }))}
/>
<RemoveButton label={`Remove level ${index + 1}`} onClick={() => write(removeAt(levels, index))} />
</div>
))}
<AddButton onClick={() => write([...levels, { label: String(levels.length) }])}>Add level</AddButton>
</div>
);
}
interface QuestionCardProps extends QuestionEditorProps {
index: number;
onRemove: () => void;
}
function QuestionCard({ question, index, onChange, onRemove }: QuestionCardProps) {
const instructionsId = useId();
return (
<fieldset aria-label={`Question ${index + 1}`} className="grid gap-3 rounded-md border p-3">
<div className="flex flex-wrap items-center gap-2">
<Input
aria-label="Question name"
className="min-w-0 flex-1"
value={question.name ?? ""}
placeholder="Optional question name"
onChange={(event) => onChange({ ...question, name: event.target.value })}
/>
<Select
value={question.type}
onValueChange={(type) => {
const next = QUESTION_TYPES.find((value) => value === type);
if (next) onChange(retypeDecisionQuestion(question, next));
}}
>
<SelectTrigger className="w-36" aria-label="Answer type">
<SelectValue>{TYPE_LABELS[question.type]}</SelectValue>
</SelectTrigger>
<SelectContent>
{QUESTION_TYPES.map((type) => (
<SelectItem key={type} value={type}>
{TYPE_LABELS[type]}
</SelectItem>
))}
</SelectContent>
</Select>
<RemoveButton label={`Remove question ${index + 1}`} onClick={onRemove} />
</div>
<div className="grid gap-1.5">
<Label htmlFor={instructionsId}>Instructions</Label>
<Textarea
id={instructionsId}
rows={2}
value={question.instructions ?? ""}
placeholder="What should the model decide?"
onChange={(event) => onChange({ ...question, instructions: event.target.value })}
/>
</div>
{question.type === "choice" && <Choices question={question} onChange={onChange} />}
{question.type === "score" && <Levels question={question} onChange={onChange} />}
</fieldset>
);
}
interface OpenAIDecisionsFormProps {
value: string;
onChange: (value: string) => void;
validation: PayloadValidation<OpenAIDecisionsRequest>;
onOpenJson: () => void;
}
export default function OpenAIDecisionsForm({ value, onChange, validation, onOpenJson }: OpenAIDecisionsFormProps) {
const inputId = useId();
const safetyId = useId();
const form = useMemo(() => readDecisionsForm(value), [value]);
if (form === undefined) {
return (
<FormFrame validation={validation}>
<p className="text-sm text-muted-foreground">
This request has JSON the form can&apos;t show. Edit it in JSON to fix its shape.
</p>
<Button variant="outline" className="w-fit" onClick={onOpenJson}>
Edit in JSON
</Button>
</FormFrame>
);
}
const questions = form.questions ?? [];
const write = (next: DecisionsFormPayload) => onChange(JSON.stringify(next, null, 2));
const writeQuestions = (next: DecisionsFormQuestion[]) => write({ ...form, questions: next });
return (
<FormFrame validation={validation}>
<div className="grid gap-1.5">
{Array.isArray(form.input) ? (
<>
<p className="text-sm text-muted-foreground">
Input contains messages. Edit messages and image parts in JSON; form edits keep them unchanged.
</p>
<Button variant="outline" className="w-fit" onClick={onOpenJson}>
Edit input in JSON
</Button>
</>
) : (
<>
<Label htmlFor={inputId}>Input</Label>
<Textarea
id={inputId}
rows={4}
value={form.input ?? ""}
placeholder="Paste the text the model should decide about"
onChange={(event) => write({ ...form, input: event.target.value })}
/>
</>
)}
</div>
{questions.map((question, index) => (
<QuestionCard
key={index}
index={index}
question={question}
onChange={(next) => writeQuestions(replaceAt(questions, index, next))}
onRemove={() => writeQuestions(removeAt(questions, index))}
/>
))}
<AddButton
onClick={() =>
writeQuestions([
...questions,
{
type: "predicate",
name: freeName(
questions.map((q) => q.name ?? ""),
"question",
),
instructions: "",
},
])
}
>
Add question
</AddButton>
<div className="grid gap-1.5">
<Label htmlFor={safetyId}>Safety identifier</Label>
<Input
id={safetyId}
value={form.safety_identifier ?? ""}
placeholder="Optional"
onChange={(event) => write({ ...form, safety_identifier: event.target.value || undefined })}
/>
</div>
</FormFrame>
);
}

View file

@ -1,7 +1,5 @@
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { cn } from "@/lib/cva.config";
import { ROOT_BLOCK_STYLES } from "./lib/rootBlocks";
import type { PlaygroundQuestion, PlaygroundRequest } from "./lib/schemas";
function formatState(state: unknown): string {
@ -74,10 +72,7 @@ export default function QuestionBreakdown({ payload }: { payload?: PlaygroundReq
<CardDescription>Review the state and criteria that will be sent.</CardDescription>
</CardHeader>
<CardContent className="grid gap-4 wrap-anywhere">
<section
aria-labelledby="system-one-state-heading"
className={cn("grid gap-2 border-l-2 pl-3", ROOT_BLOCK_STYLES.state.accent)}
>
<section aria-labelledby="system-one-state-heading" className="grid gap-2">
<h3 id="system-one-state-heading" className="text-sm font-medium">
State
</h3>
@ -85,7 +80,7 @@ export default function QuestionBreakdown({ payload }: { payload?: PlaygroundReq
{formatState(payload.state)}
</pre>
</section>
<div className={cn("grid gap-3 border-l-2 pl-3", ROOT_BLOCK_STYLES.questions.accent)}>
<div className="grid gap-3">
{Object.entries(payload.questions).map(([id, question]) => (
<section key={id} className="grid gap-3 rounded-md border p-3" aria-labelledby={`question-${id}`}>
<div className="flex flex-wrap items-center justify-between gap-2">

View file

@ -0,0 +1,164 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import ResponseView from "./ResponseView";
import type { SystemOneResponse } from "./lib/schemas";
import type { OpenAIDecisionsResponse } from "./lib/openAIDecisions";
const sampleResponse: SystemOneResponse = {
model: "jev-1.13.0",
answers: {
human: { type: "noul", noul: 0.99 },
dept: {
type: "choice",
choice: "technical",
confidence: 0.27,
probabilities: { billing: 0.36, technical: 0.64 },
},
frustration: {
type: "score",
score: 2,
confidence: 0.99,
legend: { "0": "calm", "1": "mild", "2": "angry" },
probabilities: { "0": 0, "1": 0, "2": 1 },
},
},
usage: { input_tokens: 378, output_tokens: 63 },
};
describe("ResponseView integration", () => {
it("shows OpenAI Decisions answer cards in Form view and the complete response in JSON view", async () => {
const response: OpenAIDecisionsResponse = {
model: "jev-latest",
answers: [
{ name: null, type: "predicate", probability: 0 },
{
name: "route",
type: "choice",
choice: false,
confidence: 0.8,
probabilities: [
{ value: false, probability: 0.8 },
{ value: "other", probability: 0.2 },
],
},
{
name: "severity",
type: "score",
score: 0,
confidence: 0,
probabilities: [
{ value: 0, label: "low", probability: 1 },
{ value: 1, label: "high", probability: 0 },
],
},
{ name: "blocked", type: "refusal" },
{ name: "blocked", type: "refusal" },
],
usage: { input_tokens: 3, output_tokens: 4, total_tokens: 7 },
request_id: "keep-extra-fields",
};
const { rerender } = render(<ResponseView response={response} isLoading={false} view="form" />);
expect(screen.getByText("Question 1")).toBeInTheDocument();
expect(screen.getByText("0% yes")).toBeInTheDocument();
expect(screen.getByRole("meter", { name: "false probability" })).toHaveAttribute("aria-valuetext", "80%, selected");
expect(screen.getByRole("meter", { name: "0: low probability" })).toHaveAttribute(
"aria-valuetext",
"100%, selected",
);
expect(screen.getByText("0% confidence")).toBeInTheDocument();
expect(screen.getAllByText("Model refused to answer")).toHaveLength(2);
expect(screen.queryByRole("region", { name: "Decisions response JSON" })).not.toBeInTheDocument();
await userEvent.click(screen.getByRole("button", { name: "Raw response" }));
expect(screen.getByRole("region", { name: "Decisions response JSON" })).toHaveTextContent("keep-extra-fields");
rerender(<ResponseView response={response} isLoading={false} view="json" />);
expect(screen.queryByRole("meter")).not.toBeInTheDocument();
expect(JSON.parse(screen.getByRole("region", { name: "Decisions response JSON" }).textContent ?? "")).toEqual(
response,
);
rerender(<ResponseView response={response} isLoading={false} view="form" />);
expect(screen.getByRole("meter", { name: "false probability" })).toHaveAttribute("aria-valuenow", "80");
});
it("shows System One raw response instead of cards in JSON view", () => {
render(<ResponseView response={sampleResponse} isLoading={false} view="json" />);
expect(screen.queryByRole("meter")).not.toBeInTheDocument();
expect(JSON.parse(screen.getByRole("region", { name: "Decisions response JSON" }).textContent ?? "")).toEqual(
sampleResponse,
);
});
it("renders calibrated probabilities for choice, noul, and score answers", () => {
render(<ResponseView response={sampleResponse} isLoading={false} latencyMs={120} />);
expect(screen.getAllByText("technical")).toHaveLength(2);
const meters = screen.getAllByRole("meter");
expect(meters.map((meter) => meter.getAttribute("aria-label"))).toEqual([
"Yes probability",
"technical probability",
"billing probability",
"0: calm probability",
"1: mild probability",
"2: angry probability",
]);
expect(screen.getByRole("meter", { name: "technical probability" })).toHaveAttribute("aria-valuenow", "64");
expect(screen.getByRole("meter", { name: "billing probability" })).toHaveAttribute("aria-valuenow", "36");
expect(screen.getByText("99% yes")).toBeInTheDocument();
expect(screen.getByRole("meter", { name: "Yes probability" })).toHaveAttribute("aria-valuenow", "99");
expect(screen.getByRole("meter", { name: "2: angry probability" })).toBeInTheDocument();
expect(screen.getByText("378 input / 63 output tokens")).toBeInTheDocument();
});
it("selects the score level nearest a fractional calibrated score", () => {
const fractional: SystemOneResponse = {
answers: {
severity: { type: "score", score: 2.39, probabilities: { "1": 0, "2": 0.62, "3": 0.38 } },
},
};
render(<ResponseView response={fractional} isLoading={false} />);
expect(screen.getByRole("meter", { name: "2 probability" })).toHaveAttribute("aria-valuetext", "62%, selected");
expect(screen.getByRole("meter", { name: "3 probability" })).toHaveAttribute("aria-valuetext", "38%");
expect(screen.getByRole("meter", { name: "1 probability" })).toHaveAttribute("aria-valuetext", "0%");
});
it("rounds the calibrated score pill to two decimals and leaves integers bare", () => {
const precise: SystemOneResponse = {
answers: {
severity: { type: "score", score: 3.9809950180008897, probabilities: { "3": 0.02, "4": 0.98 } },
clarity: { type: "score", score: 2, probabilities: { "2": 1 }, legend: { "2": "Clear enough" } },
},
};
render(<ResponseView response={precise} isLoading={false} />);
expect(screen.getByText("3.98")).toBeInTheDocument();
expect(screen.queryByText("3.9809950180008897")).not.toBeInTheDocument();
expect(screen.getByText("2")).toBeInTheDocument();
});
it("renders structured decision score legends without coercing objects to strings", () => {
const response: SystemOneResponse = {
answers: {
severity: {
type: "score",
score: 0,
probabilities: { "0": 1 },
legend: { "0": { description: "Low severity" } },
},
},
usage: null,
};
render(<ResponseView response={response} isLoading={false} />);
expect(screen.getByRole("meter", { name: '0: {"description":"Low severity"} probability' })).toHaveAttribute(
"aria-valuenow",
"100",
);
expect(screen.queryByText(/\[object Object\]/)).not.toBeInTheDocument();
});
it("shows an inline error message", () => {
render(<ResponseView error="Upstream request failed" isLoading={false} />);
expect(screen.getByRole("alert")).toHaveTextContent("Upstream request failed");
});
});

View file

@ -1,87 +0,0 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import ResponseView from "./ResponseView";
import type { SystemOneResponse } from "./lib/schemas";
const sampleResponse: SystemOneResponse = {
model: "jev-1.13.0",
answers: {
human: { type: "noul", noul: 0.99 },
dept: {
type: "choice",
choice: "technical",
confidence: 0.27,
probabilities: { billing: 0.36, technical: 0.64 },
},
frustration: {
type: "score",
score: 2,
confidence: 0.99,
legend: { "0": "calm", "1": "mild", "2": "angry" },
probabilities: { "0": 0, "1": 0, "2": 1 },
},
},
usage: { input_tokens: 378, output_tokens: 63 },
};
describe("ResponseView", () => {
it("renders calibrated probabilities for choice, noul, and score answers", () => {
render(<ResponseView response={sampleResponse} isLoading={false} latencyMs={120} />);
expect(screen.getAllByText("technical")).toHaveLength(2);
const meters = screen.getAllByRole("meter");
expect(meters.map((meter) => meter.getAttribute("aria-label"))).toEqual([
"Yes probability",
"technical probability",
"billing probability",
"0: calm probability",
"1: mild probability",
"2: angry probability",
]);
expect(screen.getByRole("meter", { name: "technical probability" })).toHaveAttribute("aria-valuenow", "64");
expect(screen.getByRole("meter", { name: "billing probability" })).toHaveAttribute("aria-valuenow", "36");
expect(screen.getByText("99% yes")).toBeInTheDocument();
expect(screen.getByRole("meter", { name: "Yes probability" })).toHaveAttribute("aria-valuenow", "99");
expect(screen.getByRole("meter", { name: "2: angry probability" })).toBeInTheDocument();
expect(screen.getByText("378 input / 63 output tokens")).toBeInTheDocument();
});
it("selects the score level nearest a fractional calibrated score", () => {
const fractional: SystemOneResponse = {
answers: {
severity: { type: "score", score: 2.39, probabilities: { "1": 0, "2": 0.62, "3": 0.38 } },
},
};
render(<ResponseView response={fractional} isLoading={false} />);
expect(screen.getByRole("meter", { name: "2 probability" })).toHaveAttribute("aria-valuetext", "62%, selected");
expect(screen.getByRole("meter", { name: "3 probability" })).toHaveAttribute("aria-valuetext", "38%");
expect(screen.getByRole("meter", { name: "1 probability" })).toHaveAttribute("aria-valuetext", "0%");
});
it("renders structured decision score legends without coercing objects to strings", () => {
const response: SystemOneResponse = {
answers: {
severity: {
type: "score",
score: 0,
probabilities: { "0": 1 },
legend: { "0": { description: "Low severity" } },
},
},
usage: null,
};
render(<ResponseView response={response} isLoading={false} />);
expect(screen.getByRole("meter", { name: '0: {"description":"Low severity"} probability' })).toHaveAttribute(
"aria-valuenow",
"100",
);
expect(screen.queryByText(/\[object Object\]/)).not.toBeInTheDocument();
});
it("shows an inline error message", () => {
render(<ResponseView error="Upstream request failed" isLoading={false} />);
expect(screen.getByRole("alert")).toHaveTextContent("Upstream request failed");
});
});

View file

@ -6,9 +6,13 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/component
import { ChevronDown, LoaderCircle } from "lucide-react";
import { useState } from "react";
import type { SystemOneAnswer, SystemOneResponse } from "./lib/schemas";
import type { OpenAIDecisionsResponse } from "./lib/openAIDecisions";
const SCORE_FORMAT = new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 });
interface ResponseViewProps {
response?: SystemOneResponse;
response?: SystemOneResponse | OpenAIDecisionsResponse;
view?: "form" | "json";
fallbackModel?: string;
latencyMs?: number;
error?: string;
@ -85,7 +89,7 @@ function AnswerDetails({ answer }: { answer: SystemOneAnswer }) {
<div className="grid gap-4">
<div className="flex flex-wrap items-center gap-2">
<span className="text-xs text-muted-foreground">Score</span>
<Badge>{answer.score}</Badge>
<Badge>{SCORE_FORMAT.format(answer.score)}</Badge>
{answer.confidence !== undefined && (
<Badge variant="outline">{Math.round(answer.confidence * 100)}% confidence</Badge>
)}
@ -111,7 +115,87 @@ function AnswerDetails({ answer }: { answer: SystemOneAnswer }) {
);
}
export default function ResponseView({ response, fallbackModel, latencyMs, error, isLoading }: ResponseViewProps) {
function OpenAIAnswerDetails({ answer }: { answer: OpenAIDecisionsResponse["answers"][number] }) {
switch (answer.type) {
case "refusal":
return <p className="text-sm text-muted-foreground">Model refused to answer</p>;
case "predicate":
return <AnswerDetails answer={{ type: "noul", noul: answer.probability }} />;
case "choice":
return (
<AnswerDetails
answer={{
type: "choice",
choice: String(answer.choice),
confidence: answer.confidence,
probabilities: Object.fromEntries(
answer.probabilities.map(({ value, probability }) => [String(value), probability]),
),
}}
/>
);
case "score":
return (
<AnswerDetails
answer={{
type: "score",
score: answer.score,
confidence: answer.confidence,
probabilities: Object.fromEntries(
answer.probabilities.map(({ value, probability }) => [String(value), probability]),
),
legend: Object.fromEntries(answer.probabilities.map(({ value, label }) => [String(value), label])),
}}
/>
);
}
}
function RawResponse({ response }: { response: NonNullable<ResponseViewProps["response"]> }) {
return (
<section aria-label="Decisions response JSON">
<pre className="whitespace-pre-wrap wrap-anywhere rounded-md bg-muted p-3 font-mono text-xs">
{JSON.stringify(response, null, 2)}
</pre>
</section>
);
}
function AnswerCards({ answers }: { answers: SystemOneResponse["answers"] | OpenAIDecisionsResponse["answers"] }) {
const cards = Array.isArray(answers)
? answers.map((answer, index) => ({
id: index,
name: answer.name ?? `Question ${index + 1}`,
type: answer.type,
details: <OpenAIAnswerDetails answer={answer} />,
}))
: Object.entries(answers).map(([id, answer]) => ({
id,
name: id,
type: answer.type,
details: <AnswerDetails answer={answer} />,
}));
return cards.map(({ id, name, type, details }) => (
<Card key={id} size="sm">
<CardHeader>
<CardTitle className="font-mono">{name}</CardTitle>
<CardAction>
<Badge variant="secondary">{type}</Badge>
</CardAction>
</CardHeader>
<CardContent>{details}</CardContent>
</Card>
));
}
export default function ResponseView({
response,
view = "form",
fallbackModel,
latencyMs,
error,
isLoading,
}: ResponseViewProps) {
const [showRaw, setShowRaw] = useState(false);
if (isLoading) {
@ -128,7 +212,7 @@ export default function ResponseView({ response, fallbackModel, latencyMs, error
if (error) {
return (
<Alert variant="destructive">
<AlertTitle>System One request failed</AlertTitle>
<AlertTitle>Decisions request failed</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
);
@ -139,7 +223,7 @@ export default function ResponseView({ response, fallbackModel, latencyMs, error
<Card>
<CardHeader>
<CardTitle>Calibrated probabilities</CardTitle>
<CardDescription>Send a request to see System One answers and probabilities.</CardDescription>
<CardDescription>Send a request to see decision answers and probabilities.</CardDescription>
</CardHeader>
</Card>
);
@ -152,7 +236,7 @@ export default function ResponseView({ response, fallbackModel, latencyMs, error
<CardHeader>
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="grid gap-1">
<CardTitle>Calibrated probabilities</CardTitle>
<CardTitle>{view === "json" ? "Response JSON" : "Calibrated probabilities"}</CardTitle>
{model && <CardDescription>{model}</CardDescription>}
</div>
<div className="flex flex-wrap items-center gap-2 text-xs">
@ -166,34 +250,22 @@ export default function ResponseView({ response, fallbackModel, latencyMs, error
</div>
</CardHeader>
<CardContent className="grid gap-3">
{Object.entries(response.answers).map(([id, answer]) => (
<Card key={id} size="sm">
<CardHeader>
<CardTitle className="font-mono">{id}</CardTitle>
<CardAction>
<Badge variant="secondary">{answer.type}</Badge>
</CardAction>
</CardHeader>
<CardContent>
<AnswerDetails answer={answer} />
</CardContent>
</Card>
))}
<Collapsible open={showRaw} onOpenChange={setShowRaw}>
<CollapsibleTrigger
render={
<Button variant="ghost" size="sm">
<ChevronDown className="size-4" />
Raw response
</Button>
}
/>
<CollapsibleContent>
<pre className="whitespace-pre-wrap wrap-anywhere rounded-md bg-muted p-3 font-mono text-xs">
{JSON.stringify(response, null, 2)}
</pre>
</CollapsibleContent>
</Collapsible>
{view === "json" ? <RawResponse response={response} /> : <AnswerCards answers={response.answers} />}
{view === "form" && (
<Collapsible open={showRaw} onOpenChange={setShowRaw}>
<CollapsibleTrigger
render={
<Button variant="ghost" size="sm">
<ChevronDown className="size-4" />
Raw response
</Button>
}
/>
<CollapsibleContent>
<RawResponse response={response} />
</CollapsibleContent>
</Collapsible>
)}
</CardContent>
</Card>
);

View file

@ -5,10 +5,8 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/cva.config";
import { Plus, Trash2 } from "lucide-react";
import { useId, useMemo, useState, type ReactNode } from "react";
import { IssueList, ValidationStatus } from "./JsonEditor";
import { useId, useMemo, useState } from "react";
import { AddButton, FormFrame, RemoveButton } from "./DecisionFormControls";
import {
blankQuestion,
freeName,
@ -29,7 +27,12 @@ import {
import type { SystemOnePayloadValidation } from "./lib/validatePayload";
const QUESTION_TYPES: readonly QuestionType[] = ["choice", "noul", "score"];
const TYPE_LABELS: Record<QuestionType, string> = { choice: "Choice", noul: "Yes / no", score: "Score" };
const TYPE_LABELS: Record<QuestionType, string> = { choice: "Choice", noul: "noul", score: "Score" };
const TYPE_HELP: Record<QuestionType, string | undefined> = {
choice: undefined,
noul: "noul is a yes / no question",
score: undefined,
};
const isQuestionType = (value: unknown): value is QuestionType => QUESTION_TYPES.some((type) => type === value);
@ -97,23 +100,6 @@ function KeyInput({ value, taken, onCommit, label, placeholder }: KeyInputProps)
);
}
function RemoveButton({ label, onClick }: { label: string; onClick: () => void }) {
return (
<Button variant="ghost" size="icon" aria-label={label} title={label} onClick={onClick}>
<Trash2 />
</Button>
);
}
function AddButton({ children, onClick }: { children: ReactNode; onClick: () => void }) {
return (
<Button variant="outline" size="sm" className="w-fit" onClick={onClick}>
<Plus />
{children}
</Button>
);
}
function ChoiceOptions({ question, onChange }: QuestionEditorProps<ChoiceQuestion>) {
const labels = Object.keys(question.criteria);
const writeCriteria = (criteria: Record<string, string>) => onChange({ ...question, criteria });
@ -238,7 +224,7 @@ function QuestionCard({ name, question, taken, onChange, onRename, onRemove }: Q
</SelectTrigger>
<SelectContent>
{QUESTION_TYPES.map((type) => (
<SelectItem key={type} value={type}>
<SelectItem key={type} value={type} title={TYPE_HELP[type]}>
{TYPE_LABELS[type]}
</SelectItem>
))}
@ -261,26 +247,6 @@ function QuestionCard({ name, question, taken, onChange, onRename, onRemove }: Q
);
}
function FormFrame({ validation, children }: { validation: SystemOnePayloadValidation; children: ReactNode }) {
const issuesId = useId();
return (
<div
className={cn(
"flex min-h-96 flex-1 flex-col overflow-hidden rounded-md border bg-background",
!validation.isValid && "border-destructive/60",
)}
>
<div className="flex items-center gap-2 border-b px-3 py-2">
<span className="text-sm font-medium">Request form</span>
<ValidationStatus validation={validation} />
</div>
<div className="grid min-h-80 flex-1 content-start gap-4 overflow-auto p-3">{children}</div>
<IssueList id={issuesId} validation={validation} />
</div>
);
}
export default function SystemOneForm({ value, onChange, validation, onOpenJson }: SystemOneFormProps) {
const stateId = useId();
const form = useMemo(() => readForm(value), [value]);

View file

@ -11,8 +11,9 @@ import {
import type { ReactElement } from "react";
import userEvent from "@testing-library/user-event";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import SystemOneUI from "./SystemOneUI";
import SystemOneUI, { NOUL_HELP } from "./SystemOneUI";
import type { SystemOneResponse } from "./lib/schemas";
import type { OpenAIDecisionsResponse } from "./lib/openAIDecisions";
interface ModelGroupInfo {
model_group: string;
@ -64,7 +65,19 @@ const openJsonEditor = async () => {
return screen.getByRole("textbox", { name: "System One JSON payload" });
};
const createResponse = (body: SystemOneResponse, status = 200, errorText = "") =>
const selectEndpoint = async (endpoint: "/v1/decisions" | "/v1/systemone") => {
screen.getByRole("combobox", { name: "Decision endpoint" }).focus();
await userEvent.keyboard("{ArrowDown}");
await userEvent.click(await screen.findByRole("option", { name: endpoint, exact: true }));
};
const selectPreset = async (label: string) => {
screen.getByRole("combobox", { name: "Example" }).focus();
await userEvent.keyboard("{ArrowDown}");
await userEvent.click(await screen.findByRole("option", { name: label }));
};
const createResponse = (body: SystemOneResponse | OpenAIDecisionsResponse, status = 200, errorText = "") =>
({
ok: status >= 200 && status < 300,
status,
@ -92,28 +105,94 @@ describe("SystemOneUI integration", () => {
document.cookie = "token=; path=/; max-age=0";
});
it("restores the example after the request is edited", async () => {
const user = userEvent.setup();
it("switches between example presets and reads Custom once the request is edited", async () => {
render(<SystemOneUI accessToken="session-key" />);
const editor = await openJsonEditor();
const example = (editor as HTMLTextAreaElement).value;
const resetButton = screen.getByRole("button", { name: "Reset example" });
expect(resetButton).toBeDisabled();
const picker = screen.getByRole("combobox", { name: "Example" });
expect(picker).toHaveTextContent("Triage a bug report");
fireEvent.change(editor, { target: { value: "{}" } });
expect(resetButton).toBeEnabled();
await user.click(resetButton);
expect(picker).toHaveTextContent("Custom");
await selectPreset("Triage a bug report");
expect(editor).toHaveValue(example);
expect(resetButton).toBeDisabled();
expect(picker).toHaveTextContent("Triage a bug report");
await selectPreset("Score a sales lead");
expect(picker).toHaveTextContent("Score a sales lead");
expect(JSON.parse((editor as HTMLTextAreaElement).value)).toMatchObject({ questions: { fit: { type: "score" } } });
});
it("explains the native decision endpoint after opting in without announcing it as an alert", async () => {
it("clears the whole request and lets a preset be picked from nothing", async () => {
const user = userEvent.setup();
render(<SystemOneUI accessToken="session-key" />);
const clear = screen.getByRole("button", { name: "Clear" });
expect(screen.getByRole("textbox", { name: "Input" })).toHaveDisplayValue(/streaming responses/);
await user.click(clear);
expect(screen.getByRole("textbox", { name: "Input" })).toHaveValue("");
expect(screen.queryAllByRole("group", { name: /Question / })).toHaveLength(0);
expect(screen.getByRole("button", { name: "Send" })).toBeDisabled();
expect(clear).toBeDisabled();
expect(screen.getByRole("combobox", { name: "Example" })).toHaveTextContent("Custom");
await selectPreset("Route a support ticket");
expect(screen.getByRole("textbox", { name: "Input" })).toHaveDisplayValue(/charged twice/);
expect(screen.getAllByRole("group", { name: /Question / })).toHaveLength(1);
expect(screen.getByRole("button", { name: "Send" })).toBeEnabled();
});
it("keeps the preset label after picking a model and leaves an omitted model out when clearing", async () => {
mockModelLookup.mockResolvedValue(modelGroupInfoResponse(DECISION_AND_CHAT_MODELS));
render(<SystemOneUI accessToken="session-key" />);
await waitFor(() => expect(screen.getByRole("combobox", { name: "Decision model" })).toHaveValue("jev-latest"));
const picker = screen.getByRole("combobox", { name: "Example" });
const editor = await openJsonEditor();
const example = JSON.parse((editor as HTMLTextAreaElement).value) as Record<string, unknown>;
const { model: _model, ...withoutModel } = example;
fireEvent.change(editor, {
target: { value: JSON.stringify({ ...withoutModel, model: "pplx-decider" }, null, 2) },
});
expect(picker).toHaveTextContent("Triage a bug report");
fireEvent.change(editor, { target: { value: JSON.stringify(withoutModel, null, 2) } });
expect(picker).toHaveTextContent("Triage a bug report");
await userEvent.click(screen.getByRole("button", { name: "Clear" }));
expect(JSON.parse((editor as HTMLTextAreaElement).value)).toEqual({ state: "", questions: {} });
await selectPreset("Score a sales lead");
expect(JSON.parse((editor as HTMLTextAreaElement).value)).not.toHaveProperty("model");
});
it("offers the same presets on /v1/decisions in the OpenAI shape and clears to an empty input", async () => {
const user = userEvent.setup();
render(<SystemOneUI accessToken="session-key" />);
await selectEndpoint("/v1/decisions");
await selectPreset("Moderate a forum comment");
await user.click(screen.getByRole("tab", { name: "JSON" }));
const editor = screen.getByRole("textbox", { name: "Decisions JSON payload" }) as HTMLTextAreaElement;
const payload = JSON.parse(editor.value);
expect(payload.input).toMatch(/stream=false/);
expect(payload.questions).toEqual([
{ type: "predicate", name: "breaks_rules", instructions: "Does this comment break the community guidelines?" },
]);
await user.click(screen.getByRole("button", { name: "Clear" }));
expect(JSON.parse(editor.value)).toEqual({ input: "", questions: [] });
expect(screen.getByRole("button", { name: "Send" })).toBeDisabled();
});
it("offers both literal proxy endpoints without TypeSafe or product prefixes", async () => {
render(<SystemOneUI accessToken="session-key" />);
expect(screen.getByRole("combobox", { name: "Decision endpoint" })).toHaveTextContent("/v1/systemone");
screen.getByRole("combobox", { name: "Decision endpoint" }).focus();
await user.keyboard("{ArrowDown}");
await user.click(await screen.findByRole("option", { name: "System One · /v1/systemone" }));
await userEvent.keyboard("{ArrowDown}");
expect(await screen.findByRole("option", { name: "/v1/decisions", exact: true })).toBeInTheDocument();
expect(screen.getByRole("option", { name: "/v1/systemone", exact: true })).toBeInTheDocument();
expect(screen.getAllByRole("option")).toHaveLength(2);
expect(screen.queryByText(/TypeSafe/)).not.toBeInTheDocument();
await userEvent.keyboard("{Escape}");
expect(screen.getByRole("note", { name: "Decision endpoint notice" })).toHaveTextContent(
"omit model to use the proxy's configured default.",
@ -134,7 +213,7 @@ describe("SystemOneUI integration", () => {
mockModelLookup.mockResolvedValue(modelGroupInfoResponse(DECISION_AND_CHAT_MODELS));
render(<SystemOneUI accessToken="session-key" />);
expect(await screen.findByRole("combobox", { name: "Decision model" })).toHaveValue("jev-latest");
await waitFor(() => expect(screen.getByRole("combobox", { name: "Decision model" })).toHaveValue("jev-latest"));
expect(lookupAuthHeaders(mockModelLookup.mock.calls)).toEqual([expect.arrayContaining(["Bearer session-key"])]);
await user.click(screen.getByRole("button", { name: "Send" }));
@ -147,6 +226,297 @@ describe("SystemOneUI integration", () => {
});
});
it("sends each endpoint's real shape and switches existing responses between cards and JSON without resending", async () => {
const user = userEvent.setup();
mockModelLookup.mockResolvedValue(modelGroupInfoResponse(DECISION_AND_CHAT_MODELS));
render(<SystemOneUI accessToken="session-key" />);
await waitFor(() => expect(screen.getByRole("combobox", { name: "Decision model" })).toHaveValue("jev-latest"));
const systemOneEditor = await openJsonEditor();
const systemOneDraft = JSON.stringify({
model: "jev-latest",
state: "An outage",
questions: { urgent: { type: "noul", instructions: "Is it urgent?" } },
});
fireEvent.change(systemOneEditor, { target: { value: systemOneDraft } });
await selectEndpoint("/v1/decisions");
const editor = screen.getByRole("textbox", { name: "Decisions JSON payload" });
const nativeExample = (editor as HTMLTextAreaElement).value;
expect(JSON.parse(nativeExample)).toMatchObject({
input: expect.any(String),
questions: expect.arrayContaining([expect.objectContaining({ type: "predicate" })]),
});
expect(screen.getByRole("tab", { name: "Form" })).toBeInTheDocument();
const payload = {
model: "jev-latest",
input: "An outage",
questions: [{ name: "urgent", type: "predicate", instructions: "Is it urgent?" }],
safety_identifier: "qa-session",
};
fireEvent.change(editor, { target: { value: JSON.stringify(payload) } });
const response: OpenAIDecisionsResponse = {
model: "jev-latest",
answers: [{ name: "urgent", type: "predicate", probability: 0.95 }],
usage: { input_tokens: 10, output_tokens: 1, total_tokens: 11 },
};
mockFetch.mockResolvedValueOnce(createResponse(response));
await user.click(screen.getByRole("button", { name: "Send" }));
const result = await screen.findByRole("region", { name: "Decisions response JSON" });
expect(result).toHaveTextContent('"probability": 0.95');
expect(result).toHaveTextContent('"answers": [');
expect(mockFetch.mock.calls[0]?.[0]).toMatch(/\/v1\/decisions$/);
expect(JSON.parse(mockFetch.mock.calls[0]?.[1]?.body as string)).toEqual(payload);
expect(screen.queryByRole("meter")).not.toBeInTheDocument();
await user.click(screen.getByRole("tab", { name: "Form" }));
expect(screen.getByRole("meter", { name: "Yes probability" })).toHaveAttribute("aria-valuenow", "95");
expect(screen.queryByRole("region", { name: "Decisions response JSON" })).not.toBeInTheDocument();
await user.click(screen.getByRole("tab", { name: "JSON" }));
expect(screen.getByRole("region", { name: "Decisions response JSON" })).toHaveTextContent('"probability": 0.95');
expect(mockFetch).toHaveBeenCalledTimes(1);
await selectEndpoint("/v1/systemone");
expect(screen.getByRole("textbox", { name: "System One JSON payload" })).toHaveValue(systemOneDraft);
expect(screen.queryByRole("region", { name: "Decisions response JSON" })).not.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Send" }));
expect(await screen.findByRole("region", { name: "Decisions response JSON" })).toHaveTextContent(
'"choice": "technical"',
);
expect(screen.queryByRole("meter")).not.toBeInTheDocument();
await user.click(screen.getByRole("tab", { name: "Form" }));
expect(screen.getByText("Selected choice")).toBeInTheDocument();
expect(screen.getByRole("meter", { name: "technical probability" })).toHaveAttribute("aria-valuenow", "90");
await user.click(screen.getByRole("tab", { name: "JSON" }));
expect(mockFetch).toHaveBeenCalledTimes(2);
expect(mockFetch.mock.calls[1]?.[0]).toMatch(/\/v1\/systemone$/);
expect(JSON.parse(mockFetch.mock.calls[1]?.[1]?.body as string)).toEqual(JSON.parse(systemOneDraft));
await selectEndpoint("/v1/decisions");
expect(screen.getByRole("textbox", { name: "Decisions JSON payload" })).toHaveValue(JSON.stringify(payload));
await selectPreset("Triage a bug report");
expect(screen.getByRole("textbox", { name: "Decisions JSON payload" })).toHaveValue(nativeExample);
await selectEndpoint("/v1/systemone");
expect(screen.getByRole("textbox", { name: "System One JSON payload" })).toHaveValue(systemOneDraft);
});
it("validates /v1/decisions independently and preserves an invalid draft across switches", async () => {
render(<SystemOneUI accessToken="session-key" />);
await selectEndpoint("/v1/decisions");
await userEvent.click(screen.getByRole("tab", { name: "JSON" }));
const editor = screen.getByRole("textbox", { name: "Decisions JSON payload" });
fireEvent.change(editor, { target: { value: "{" } });
expect(screen.getByRole("button", { name: "Send" })).toBeDisabled();
await selectEndpoint("/v1/systemone");
expect(screen.getByRole("button", { name: "Send" })).toBeEnabled();
await selectEndpoint("/v1/decisions");
expect(screen.getByRole("textbox", { name: "Decisions JSON payload" })).toHaveValue("{");
expect(screen.getByRole("button", { name: "Send" })).toBeDisabled();
expect(mockFetch).not.toHaveBeenCalled();
});
it("offers a Decisions form that sends input and typed questions and stays in sync with JSON", async () => {
const user = userEvent.setup();
mockFetch.mockResolvedValueOnce(
createResponse({
model: "jev-latest",
answers: [],
usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 },
}),
);
render(<SystemOneUI accessToken="session-key" />);
await selectEndpoint("/v1/decisions");
expect(screen.getByRole("tab", { name: "Form" })).toHaveAttribute("aria-selected", "true");
expect(screen.queryByRole("button", { name: "Format JSON" })).not.toBeInTheDocument();
fireEvent.change(screen.getByRole("textbox", { name: "Input" }), { target: { value: "Login is broken" } });
const firstQuestion = within(screen.getByRole("group", { name: "Question 1" }));
fireEvent.change(firstQuestion.getByRole("textbox", { name: "Question name" }), { target: { value: "team" } });
fireEvent.change(firstQuestion.getByRole("textbox", { name: "Instructions" }), {
target: { value: "Which team owns this?" },
});
fireEvent.change(firstQuestion.getByRole("textbox", { name: "Choice 1 value" }), {
target: { value: "infra" },
});
fireEvent.change(screen.getByRole("textbox", { name: "Safety identifier" }), {
target: { value: "qa-session" },
});
await user.click(screen.getByRole("tab", { name: "JSON" }));
const editor = screen.getByRole("textbox", { name: "Decisions JSON payload" });
const payload = JSON.parse((editor as HTMLTextAreaElement).value);
expect(payload).toMatchObject({ input: "Login is broken", safety_identifier: "qa-session" });
const expectedQuestion = {
name: "team",
type: "choice",
instructions: "Which team owns this?",
choices: expect.arrayContaining([expect.objectContaining({ value: "infra" })]),
};
expect(payload.questions[0]).toMatchObject(expectedQuestion);
expect(payload).not.toHaveProperty("state");
fireEvent.change(editor, { target: { value: JSON.stringify({ ...payload, input: "Fixed in JSON" }) } });
await user.click(screen.getByRole("tab", { name: "Form" }));
expect(screen.getByRole("textbox", { name: "Input" })).toHaveValue("Fixed in JSON");
await user.click(screen.getByRole("button", { name: "Send" }));
await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(1));
expect(mockFetch.mock.calls[0]?.[0]).toMatch(/\/v1\/decisions$/);
expect(JSON.parse(mockFetch.mock.calls[0]?.[1]?.body as string)).toEqual({ ...payload, input: "Fixed in JSON" });
expect(await screen.findByRole("button", { name: "Raw response" })).toBeInTheDocument();
await user.click(screen.getByRole("tab", { name: "JSON" }));
expect(screen.getByRole("region", { name: "Decisions response JSON" })).toBeInTheDocument();
});
it("adds and removes Decisions questions, boolean choices and score levels without stale fields", async () => {
const user = userEvent.setup();
render(<SystemOneUI accessToken="session-key" />);
await selectEndpoint("/v1/decisions");
const question = (index: number) => within(screen.getByRole("group", { name: `Question ${index}` }));
const pick = async (control: HTMLElement, label: string) => {
control.focus();
await user.keyboard("{ArrowDown}");
await user.click(await screen.findByRole("option", { name: label, exact: true }));
};
await user.click(screen.getByRole("button", { name: "Add question" }));
fireEvent.change(question(4).getByRole("textbox", { name: "Instructions" }), {
target: { value: "Should we escalate?" },
});
await pick(question(4).getByRole("combobox", { name: "Answer type" }), "Choice");
await user.click(question(4).getByRole("button", { name: "Remove choice 1" }));
expect(question(4).getByRole("textbox", { name: "Choice 1 value" })).toHaveValue("option_2");
expect(screen.getByRole("button", { name: "Send" })).toBeDisabled();
await user.click(question(4).getByRole("button", { name: "Add choice" }));
await pick(question(4).getByRole("combobox", { name: "Choice 1 value type" }), "false (boolean)");
fireEvent.change(question(4).getByRole("textbox", { name: "Choice 1 description" }), {
target: { value: "Not urgent" },
});
expect(screen.getByRole("button", { name: "Send" })).toBeEnabled();
await user.click(screen.getByRole("tab", { name: "JSON" }));
const choicePayload = JSON.parse(
(screen.getByRole("textbox", { name: "Decisions JSON payload" }) as HTMLTextAreaElement).value,
);
const expectedChoice = {
type: "choice",
name: "question_1",
instructions: "Should we escalate?",
choices: [{ value: false, description: "Not urgent" }, { value: "option_1" }],
};
expect(choicePayload.questions[3]).toEqual(expectedChoice);
await user.click(screen.getByRole("tab", { name: "Form" }));
await pick(question(4).getByRole("combobox", { name: "Answer type" }), "Score");
fireEvent.change(question(4).getByRole("textbox", { name: "Level 1 label" }), { target: { value: "low" } });
fireEvent.change(question(4).getByRole("textbox", { name: "Level 1 description" }), {
target: { value: "No impact" },
});
await user.click(question(4).getByRole("button", { name: "Add level" }));
await user.click(question(4).getByRole("button", { name: "Remove level 2" }));
expect(question(4).getByRole("textbox", { name: "Level 2 label" })).toHaveValue("2");
await user.click(question(2).getByRole("button", { name: "Remove question 2" }));
expect(question(3).getByRole("textbox", { name: "Instructions" })).toHaveValue("Should we escalate?");
await user.click(screen.getByRole("tab", { name: "JSON" }));
const scorePayload = JSON.parse(
(screen.getByRole("textbox", { name: "Decisions JSON payload" }) as HTMLTextAreaElement).value,
);
expect(scorePayload.questions).toHaveLength(3);
const expectedScore = {
type: "score",
name: "question_1",
instructions: "Should we escalate?",
levels: [{ label: "low", description: "No impact" }, { label: "2" }],
};
expect(scorePayload.questions[2]).toEqual(expectedScore);
await user.click(screen.getByRole("tab", { name: "Form" }));
await pick(question(3).getByRole("combobox", { name: "Answer type" }), "Predicate");
await user.click(screen.getByRole("tab", { name: "JSON" }));
const predicatePayload = JSON.parse(
(screen.getByRole("textbox", { name: "Decisions JSON payload" }) as HTMLTextAreaElement).value,
);
expect(predicatePayload.questions[2]).toEqual({
type: "predicate",
name: "question_1",
instructions: "Should we escalate?",
});
});
it("preserves message input and extra fields in the Decisions form, including across routes and reset", async () => {
const user = userEvent.setup();
render(<SystemOneUI accessToken="session-key" />);
await selectEndpoint("/v1/decisions");
await user.click(screen.getByRole("tab", { name: "JSON" }));
const payload = {
model: "jev-latest",
metadata: { source: "playground" },
input: [
{
role: "user",
content: [
{ type: "input_text", text: "Is this urgent?" },
{ type: "input_image", image_url: "https://example.com/image.png" },
],
},
],
questions: [{ type: "predicate", name: null, instructions: "Old instructions" }],
};
fireEvent.change(screen.getByRole("textbox", { name: "Decisions JSON payload" }), {
target: { value: JSON.stringify(payload) },
});
await user.click(screen.getByRole("tab", { name: "Form" }));
expect(screen.getByRole("button", { name: "Edit input in JSON" })).toBeInTheDocument();
fireEvent.change(screen.getByRole("textbox", { name: "Instructions" }), { target: { value: "Is it urgent?" } });
await selectEndpoint("/v1/systemone");
const nativeInput = (screen.getByRole("textbox", { name: "Input" }) as HTMLTextAreaElement).value;
await selectEndpoint("/v1/decisions");
expect(screen.getByRole("textbox", { name: "Instructions" })).toHaveValue("Is it urgent?");
await user.click(screen.getByRole("button", { name: "Edit input in JSON" }));
const next = JSON.parse(
(screen.getByRole("textbox", { name: "Decisions JSON payload" }) as HTMLTextAreaElement).value,
);
expect(next).toEqual({ ...payload, questions: [{ ...payload.questions[0], instructions: "Is it urgent?" }] });
await user.click(screen.getByRole("tab", { name: "Form" }));
await selectPreset("Triage a bug report");
expect(screen.getByRole("textbox", { name: "Input" })).toBeInTheDocument();
expect(screen.getAllByRole("group", { name: /Question \d/ })).toHaveLength(3);
await selectEndpoint("/v1/systemone");
expect(screen.getByRole("textbox", { name: "Input" })).toHaveValue(nativeInput);
});
it("keeps malformed Decisions JSON intact when switching to its form", async () => {
const user = userEvent.setup();
render(<SystemOneUI accessToken="session-key" />);
await selectEndpoint("/v1/decisions");
await user.click(screen.getByRole("tab", { name: "JSON" }));
fireEvent.change(screen.getByRole("textbox", { name: "Decisions JSON payload" }), { target: { value: "{" } });
await user.click(screen.getByRole("tab", { name: "Form" }));
expect(screen.getByRole("button", { name: "Send" })).toBeDisabled();
await user.click(screen.getByRole("button", { name: "Edit in JSON" }));
expect(screen.getByRole("textbox", { name: "Decisions JSON payload" })).toHaveValue("{");
});
it.each(["success", "error"])("cancels the previous endpoint request and ignores a late %s", async (outcome) => {
const pending = Promise.withResolvers<Response>();
mockFetch.mockReturnValueOnce(pending.promise);
const queryClient = new QueryClient({ defaultOptions: { mutations: { retry: false } } });
rtlRender(
<QueryClientProvider client={queryClient}>
<SystemOneUI accessToken="session-key" />
</QueryClientProvider>,
);
await userEvent.click(screen.getByRole("button", { name: "Send" }));
await screen.findByRole("button", { name: "Cancel request" });
await selectEndpoint("/v1/decisions");
expect(mockFetch.mock.calls[0]?.[1]?.signal?.aborted).toBe(true);
await act(async () => {
if (outcome === "success") {
pending.resolve(createResponse({ model: "stale-model", answers: {} }));
} else {
pending.reject(new Error("Stale request failed"));
}
await pending.promise.catch(() => undefined);
});
await waitFor(() => expect(queryClient.isMutating()).toBe(0));
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
expect(screen.queryByText("stale-model")).not.toBeInTheDocument();
expect(screen.getByRole("tab", { name: "Form" })).toHaveAttribute("aria-selected", "true");
expect(screen.getByRole("button", { name: "Send" })).toBeEnabled();
});
it("offers only decision models and writes the picked one into the request", async () => {
const user = userEvent.setup();
mockModelLookup.mockResolvedValue(modelGroupInfoResponse(DECISION_AND_CHAT_MODELS));
@ -165,16 +535,31 @@ describe("SystemOneUI integration", () => {
questions: { has_repro_steps: { type: "noul" } },
});
await user.click(screen.getByRole("button", { name: "Send" }));
await user.click(screen.getByRole("tab", { name: "Form" }));
expect(await screen.findByText("Selected choice")).toBeInTheDocument();
expect(JSON.parse(mockFetch.mock.calls[0]?.[1]?.body as string).model).toBe("pplx-decider");
});
it("links to Add Model when the key cannot call any decision model", async () => {
it("routes a configured TypeSafe model through the same proxy endpoint", async () => {
const user = userEvent.setup();
mockModelLookup.mockResolvedValue(
modelGroupInfoResponse([{ model_group: "typesafe/jev-latest", mode: "evaluation" }]),
);
render(<SystemOneUI accessToken="session-key" />);
await waitFor(() =>
expect(screen.getByRole("combobox", { name: "Decision model" })).toHaveValue("typesafe/jev-latest"),
);
await user.click(screen.getByRole("button", { name: "Send" }));
expect(await screen.findByText("Selected choice")).toBeInTheDocument();
expect(mockFetch.mock.calls[0]?.[0]).toMatch(/\/v1\/systemone$/);
expect(mockFetch.mock.calls[0]?.[0]).not.toMatch(/\/typesafe\//);
expect(JSON.parse(mockFetch.mock.calls[0]?.[1]?.body as string).model).toBe("typesafe/jev-latest");
});
it("links to Add Model when the key cannot call any decision model", async () => {
render(<SystemOneUI accessToken="session-key" />);
screen.getByRole("combobox", { name: "Decision endpoint" }).focus();
await user.keyboard("{ArrowDown}");
await user.click(await screen.findByRole("option", { name: "System One · /v1/systemone" }));
expect(await screen.findByRole("link", { name: "Add a decision model" })).toHaveAttribute(
"href",
@ -193,7 +578,7 @@ describe("SystemOneUI integration", () => {
await user.type(screen.getByLabelText("Virtual Key", { exact: true }), "sk-typed-key");
expect(await screen.findByRole("combobox", { name: "Decision model" })).toHaveValue("jev-latest");
await waitFor(() => expect(screen.getByRole("combobox", { name: "Decision model" })).toHaveValue("jev-latest"));
expect(lookupAuthHeaders(mockModelLookup.mock.calls)).toEqual([expect.arrayContaining(["Bearer sk-typed-key"])]);
});
@ -209,10 +594,10 @@ describe("SystemOneUI integration", () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
expect(document.cookie).toContain("token=ui-session");
expect(screen.getByRole("combobox", { name: "Decision endpoint" })).toHaveTextContent("/typesafe/v1/systemone");
expect(screen.getByRole("combobox", { name: "Decision endpoint" })).toHaveTextContent("/v1/systemone");
});
it("keeps an edited TypeSafe request in place when decision models load afterwards", async () => {
it("keeps an edited request in place when decision models load afterwards", async () => {
const models = Promise.withResolvers<Response>();
mockModelLookup.mockReturnValue(models.promise);
render(<SystemOneUI accessToken="session-key" />);
@ -226,9 +611,37 @@ describe("SystemOneUI integration", () => {
});
expect(editor).toHaveValue(draft);
expect(screen.queryByRole("combobox", { name: "Decision model" })).not.toBeInTheDocument();
expect(screen.getByRole("combobox", { name: "Decision model" })).toHaveValue("");
});
it.each(["/v1/decisions", "/v1/systemone"] as const)(
"omits model while discovery is pending on %s",
async (endpoint) => {
const models = Promise.withResolvers<Response>();
mockModelLookup.mockReturnValue(models.promise);
mockFetch.mockResolvedValueOnce(
createResponse({
model: "jev-latest",
answers: endpoint === "/v1/decisions" ? [] : {},
usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 },
}),
);
render(<SystemOneUI accessToken="session-key" />);
if (endpoint === "/v1/decisions") {
await selectEndpoint(endpoint);
}
await userEvent.click(screen.getByRole("button", { name: "Send" }));
expect(await screen.findByText("jev-latest")).toBeInTheDocument();
expect(mockFetch.mock.calls[0]?.[0]).toMatch(new RegExp(`${endpoint}$`));
expect(JSON.parse(mockFetch.mock.calls[0]?.[1]?.body as string)).not.toHaveProperty("model");
await act(async () => {
models.resolve(modelGroupInfoResponse(DECISION_AND_CHAT_MODELS));
await models.promise;
});
expect(screen.getByRole("combobox", { name: "Decision model" })).toHaveValue("");
},
);
it("shows invalid JSON and disables Send", async () => {
render(<SystemOneUI accessToken="session-key" />);
@ -240,7 +653,7 @@ describe("SystemOneUI integration", () => {
expect(screen.getByRole("button", { name: "Send" })).toBeDisabled();
});
it("preserves the untouched TypeSafe default and renders calibrated answers", async () => {
it("sends to /v1/systemone even without discovered models and renders calibrated answers", async () => {
const user = userEvent.setup();
sessionStorage.setItem("customProxyBaseUrl", "https://stale.example.com/");
render(<SystemOneUI accessToken="session-key" />);
@ -251,18 +664,19 @@ describe("SystemOneUI integration", () => {
expect(await screen.findAllByText("technical")).toHaveLength(2);
expect(mockFetch).toHaveBeenCalledTimes(1);
const [url, options] = mockFetch.mock.calls[0] ?? [];
expect(url).toBe("https://tenant.example.com/typesafe/v1/systemone");
expect(url).toBe("https://tenant.example.com/v1/systemone");
expect(options?.method).toBe("POST");
expect(options?.headers).toMatchObject({ "Content-Type": "application/json" });
expect(Object.values(options?.headers as Record<string, string>)).toContain("Bearer session-key");
expect(JSON.parse(options?.body as string)).toMatchObject({
model: "jev-latest",
questions: { has_repro_steps: { type: "noul" } },
});
expect(JSON.parse(options?.body as string)).not.toHaveProperty("model");
});
it("uses the requested model when the response omits it", async () => {
const user = userEvent.setup();
mockModelLookup.mockResolvedValue(modelGroupInfoResponse(DECISION_AND_CHAT_MODELS));
mockFetch.mockResolvedValueOnce(
createResponse({
answers: responseBody.answers,
@ -270,13 +684,15 @@ describe("SystemOneUI integration", () => {
);
render(<SystemOneUI accessToken="session-key" />);
await waitFor(() => expect(screen.getByRole("combobox", { name: "Decision model" })).toHaveValue("jev-latest"));
await user.click(screen.getByRole("button", { name: "Send" }));
expect(await screen.findByText("jev-latest")).toBeInTheDocument();
expect(screen.getByText("Selected choice")).toBeInTheDocument();
});
it("keeps legacy validation when question values have invalid types", async () => {
it("validates native question values before sending", async () => {
render(<SystemOneUI accessToken="session-key" />);
fireEvent.change(await openJsonEditor(), {
@ -286,25 +702,22 @@ describe("SystemOneUI integration", () => {
questions: {
category: {
type: "choice",
instructions: { text: "Route the message" },
criteria: { support: { text: "Help" } },
instructions: 1,
criteria: { support: true },
},
},
}),
},
});
expect(screen.getByText("Instructions must be a string.")).toBeInTheDocument();
expect(screen.getByText("Choice descriptions must be strings.")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Send" })).toBeDisabled();
await userEvent.click(screen.getByRole("tab", { name: "Form" }));
expect(screen.getByText("Enter a valid request to preview its state and questions.")).toBeInTheDocument();
});
it("preserves each endpoint draft and sends legacy requests to TypeSafe", async () => {
it("sends structured native questions without changing their payload", async () => {
const user = userEvent.setup();
render(<SystemOneUI accessToken="session-key" />);
screen.getByRole("combobox", { name: "Decision endpoint" }).focus();
await user.keyboard("{ArrowDown}");
await user.click(await screen.findByRole("option", { name: "System One · /v1/systemone" }));
const editor = await openJsonEditor();
const draft = JSON.stringify({
model: "my-decider",
@ -315,33 +728,18 @@ describe("SystemOneUI integration", () => {
});
fireEvent.change(editor, { target: { value: draft } });
expect(screen.getByRole("button", { name: "Send" })).toBeEnabled();
await user.click(screen.getByRole("tab", { name: "Form" }));
expect(screen.getByText(/"text": "Help"/)).toBeInTheDocument();
screen.getByRole("combobox", { name: "Decision endpoint" }).focus();
await user.keyboard("{ArrowDown}");
await user.click(await screen.findByRole("option", { name: "TypeSafe · /typesafe/v1/systemone" }));
await user.click(screen.getByRole("button", { name: "Send" }));
expect(await screen.findByText("Selected choice")).toBeInTheDocument();
expect(mockFetch.mock.calls[0]?.[0]).toMatch(/\/typesafe\/v1\/systemone$/);
expect(JSON.parse(mockFetch.mock.calls[0]?.[1]?.body as string).model).toBe("jev-latest");
screen.getByRole("combobox", { name: "Decision endpoint" }).focus();
await user.keyboard("{ArrowDown}");
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\/systemone$/);
expect(JSON.parse(mockFetch.mock.calls[1]?.[1]?.body as string)).toEqual(JSON.parse(draft));
expect(mockFetch.mock.calls[0]?.[0]).toMatch(/\/v1\/systemone$/);
expect(JSON.parse(mockFetch.mock.calls[0]?.[1]?.body as string)).toEqual(JSON.parse(draft));
});
it("sends a native request without model so the proxy can select its default", async () => {
const user = userEvent.setup();
render(<SystemOneUI accessToken="session-key" />);
screen.getByRole("combobox", { name: "Decision endpoint" }).focus();
await user.keyboard("{ArrowDown}");
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(await openJsonEditor(), {
target: { value: JSON.stringify(payload) },
@ -355,7 +753,7 @@ describe("SystemOneUI integration", () => {
expect(body).not.toHaveProperty("model");
});
it.each(["success", "error"])("ignores a late %s after switching endpoints", async (outcome) => {
it.each(["success", "error"])("ignores a late %s after editing the request", async (outcome) => {
const user = userEvent.setup();
const pending = Promise.withResolvers<Response>();
mockFetch.mockReturnValueOnce(pending.promise);
@ -367,9 +765,7 @@ describe("SystemOneUI integration", () => {
);
await user.click(screen.getByRole("button", { name: "Send" }));
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: "System One · /v1/systemone" }));
fireEvent.change(screen.getByRole("textbox", { name: "Input" }), { target: { value: "A new request" } });
expect(mockFetch.mock.calls[0]?.[1]?.signal?.aborted).toBe(true);
expect(screen.getByRole("button", { name: "Send" })).toBeEnabled();
@ -437,7 +833,7 @@ describe("SystemOneUI integration", () => {
await user.click(screen.getByRole("button", { name: "Send" }));
expect(await screen.findByRole("alert")).toHaveTextContent("System One response has an invalid shape.");
expect(await screen.findByRole("alert")).toHaveTextContent("/v1/systemone response has an invalid shape.");
expect(screen.queryByText("jev-1.13.0")).not.toBeInTheDocument();
});
@ -524,6 +920,22 @@ describe("SystemOneUI integration", () => {
fireEvent.blur(field);
}
it("explains noul on hover in the banner and on the answer type option", async () => {
const user = userEvent.setup();
render(<SystemOneUI accessToken="session-key" />);
await user.hover(screen.getByText("noul", { selector: "span.cursor-help" }));
expect(await screen.findByText(NOUL_HELP)).toBeInTheDocument();
question("has_repro_steps").getByRole("combobox", { name: "Answer type" }).focus();
await user.keyboard("{ArrowDown}");
expect(await screen.findByRole("option", { name: "noul" })).toHaveAttribute(
"title",
"noul is a yes / no question",
);
expect(screen.getByRole("option", { name: "Choice" })).not.toHaveAttribute("title");
});
it("sends a choice and a yes/no question built in the form without opening JSON", async () => {
const user = userEvent.setup();
render(<SystemOneUI accessToken="session-key" />);
@ -545,7 +957,7 @@ describe("SystemOneUI integration", () => {
fill(question("team").getByRole("textbox", { name: "Description of infra" }), "Deploys");
await user.click(screen.getByRole("button", { name: "Add question" }));
await pickAnswerType(user, "question_1", "Yes / no");
await pickAnswerType(user, "question_1", "noul");
rename(question("question_1").getByRole("textbox", { name: "Question name" }), "urgent");
fill(question("urgent").getByRole("textbox", { name: "Instructions" }), "Is this blocking users?");
fill(question("urgent").getByRole("textbox", { name: "Yes means" }), "Nobody can log in");
@ -554,9 +966,8 @@ describe("SystemOneUI integration", () => {
expect(await screen.findByText("Selected choice")).toBeInTheDocument();
expect(screen.queryByRole("textbox", { name: "System One JSON payload" })).not.toBeInTheDocument();
expect(mockFetch.mock.calls[0]?.[0]).toMatch(/\/typesafe\/v1\/systemone$/);
expect(mockFetch.mock.calls[0]?.[0]).toMatch(/\/v1\/systemone$/);
expect(sentBody()).toEqual({
model: "jev-latest",
state: "The login page is blank after the upgrade",
questions: {
team: {
@ -575,7 +986,7 @@ describe("SystemOneUI integration", () => {
const user = userEvent.setup();
mockModelLookup.mockResolvedValue(modelGroupInfoResponse(DECISION_AND_CHAT_MODELS));
render(<SystemOneUI accessToken="session-key" />);
expect(await screen.findByRole("combobox", { name: "Decision model" })).toHaveValue("jev-latest");
await waitFor(() => expect(screen.getByRole("combobox", { name: "Decision model" })).toHaveValue("jev-latest"));
const level = question("severity").getByRole("textbox", { name: "Level 0" });
fill(level, "Typo");

View file

@ -5,21 +5,22 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { SimpleTooltip } from "@/components/ui/tooltip";
import { SearchSelect } from "@/components/shared/SearchSelect";
import { DECISIONS_DOCS_URL } from "@/lib/decisionModels";
import { uiHref } from "@/utils/uiHref";
import { useMutation } from "@tanstack/react-query";
import { Code, Info, LoaderCircle, RotateCcw, Send } from "lucide-react";
import { Code, Eraser, Info, LoaderCircle, Send } from "lucide-react";
import { useEffect, useMemo, useRef, useState } from "react";
import { makeSystemOneRequest } from "../../llm_calls/system_one";
import { PLACEHOLDER_DECISION_MODEL, SYSTEM_ONE_EXAMPLE, decisionsExample } from "./lib/example";
import { makePlaygroundDecisionRequest, type PlaygroundDecisionRequest } from "../../llm_calls/system_one";
import { DECISION_PRESETS, emptyPayload, matchPreset, presetPayload, type DecisionPreset } from "./lib/example";
import { payloadModel, withPayloadModel } from "./lib/payloadModel";
import type { DecisionEndpoint, PlaygroundRequest } from "./lib/schemas";
import JsonEditor from "./JsonEditor";
import QuestionBreakdown from "./QuestionBreakdown";
import ResponseView from "./ResponseView";
import SystemOneForm from "./SystemOneForm";
import { validateSystemOnePayload } from "./lib/validatePayload";
import OpenAIDecisionsForm from "./OpenAIDecisionsForm";
import { validateSystemOnePayload, validateOpenAIDecisionsPayload } from "./lib/validatePayload";
import { useDecisionModels, type ApiKeySource } from "./useDecisionModels";
interface SystemOneUIProps {
@ -30,15 +31,42 @@ interface SystemOneUIProps {
type EditorView = "form" | "json";
interface SystemOneSendVariables {
payload: PlaygroundRequest;
endpoint: DecisionEndpoint;
request: PlaygroundDecisionRequest;
apiKey: string;
signal: AbortSignal;
}
const EXAMPLE_PAYLOAD = JSON.stringify(SYSTEM_ONE_EXAMPLE, null, 2);
export const NOUL_HELP = "noul is a yes / no question. The model returns the probability that the answer is yes.";
const CUSTOM_PRESET = "custom";
const DECISION_MODELS_DISCUSSION_URL = "https://github.com/BerriAI/litellm/discussions/44231";
interface PresetPickerProps {
activePreset: DecisionPreset | undefined;
onPick: (id: string | null) => void;
}
function PresetPicker({ activePreset, onPick }: PresetPickerProps) {
return (
<Select value={activePreset?.id ?? CUSTOM_PRESET} onValueChange={onPick}>
<SelectTrigger className="w-56" aria-label="Example">
<SelectValue>{activePreset?.label ?? "Custom"}</SelectValue>
</SelectTrigger>
<SelectContent>
{activePreset === undefined && (
<SelectItem value={CUSTOM_PRESET} disabled>
Custom
</SelectItem>
)}
{DECISION_PRESETS.map((preset) => (
<SelectItem key={preset.id} value={preset.id}>
{preset.label}
</SelectItem>
))}
</SelectContent>
</Select>
);
}
function getCustomProxyBaseUrl(): string | undefined {
return typeof window === "undefined" ? undefined : window.sessionStorage.getItem("customProxyBaseUrl") || undefined;
}
@ -52,23 +80,27 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation =
effectiveApiKey,
getCustomProxyBaseUrl(),
);
const [chosenEndpoint, setChosenEndpoint] = useState<DecisionEndpoint | null>(null);
const endpoint: DecisionEndpoint =
chosenEndpoint ?? (decisionModels.length > 0 ? "/v1/systemone" : "/typesafe/v1/systemone");
const [drafts, setDrafts] = useState<Partial<Record<DecisionEndpoint, string>>>({});
const [endpoint, setEndpoint] = useState<PlaygroundDecisionRequest["endpoint"]>("/v1/systemone");
const [drafts, setDrafts] = useState<Partial<Record<PlaygroundDecisionRequest["endpoint"], string>>>({});
const [view, setView] = useState<EditorView>("form");
const examplePayload =
endpoint === "/v1/systemone"
? JSON.stringify(decisionsExample(decisionModels[0] ?? PLACEHOLDER_DECISION_MODEL), null, 2)
: EXAMPLE_PAYLOAD;
const rawPayload = drafts[endpoint] ?? examplePayload;
const rawPayload = drafts[endpoint] ?? presetPayload(DECISION_PRESETS[0], endpoint, decisionModels[0]);
const draftModel = drafts[endpoint] === undefined ? decisionModels[0] : payloadModel(rawPayload);
const activePreset = matchPreset(endpoint, rawPayload);
const clearedPayload = emptyPayload(endpoint, draftModel);
const activeController = useRef<AbortController | null>(null);
const validation = useMemo(() => validateSystemOnePayload(rawPayload, endpoint), [rawPayload, endpoint]);
const validated = useMemo(
() =>
endpoint === "/v1/decisions"
? ({ endpoint, validation: validateOpenAIDecisionsPayload(rawPayload) } as const)
: ({ endpoint, validation: validateSystemOnePayload(rawPayload, endpoint) } as const),
[rawPayload, endpoint],
);
const validation = validated.validation;
const hasSyntaxError = validation.issues.some((issue) => issue.path === "syntax");
const systemOne = useMutation({
mutationFn: ({ payload, apiKey, signal, endpoint }: SystemOneSendVariables) =>
makeSystemOneRequest(payload, apiKey, getCustomProxyBaseUrl(), { signal, endpoint }),
mutationFn: ({ request, apiKey, signal }: SystemOneSendVariables) =>
makePlaygroundDecisionRequest(request, apiKey, getCustomProxyBaseUrl(), { signal }),
});
const isLoading = systemOne.isPending;
const { reset: resetSystemOne } = systemOne;
@ -90,15 +122,15 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation =
function handlePayloadChange(value: string) {
if (value !== rawPayload) {
clearRequestState();
setChosenEndpoint(endpoint);
setDrafts((current) => ({ ...current, [endpoint]: value }));
}
}
function handleResetExample() {
clearRequestState();
setChosenEndpoint(endpoint);
setDrafts((current) => Object.fromEntries(Object.entries(current).filter(([key]) => key !== endpoint)));
function handlePresetPick(id: string | null) {
const preset = DECISION_PRESETS.find((candidate) => candidate.id === id);
if (preset !== undefined) {
handlePayloadChange(presetPayload(preset, endpoint, draftModel));
}
}
function handleModelPick(model: string | null) {
@ -115,14 +147,18 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation =
}
function handleSend() {
if (!validation.payload || !effectiveApiKey) {
const request: PlaygroundDecisionRequest | undefined =
validated.endpoint === "/v1/decisions"
? validated.validation.payload && { endpoint: validated.endpoint, payload: validated.validation.payload }
: validated.validation.payload && { endpoint: validated.endpoint, payload: validated.validation.payload };
if (!request || !effectiveApiKey) {
return;
}
clearRequestState();
setChosenEndpoint(endpoint);
setDrafts((current) => ({ ...current, [endpoint]: rawPayload }));
const controller = new AbortController();
activeController.current = controller;
const variables = { payload: validation.payload, apiKey: effectiveApiKey, signal: controller.signal, endpoint };
const variables = { request, apiKey: effectiveApiKey, signal: controller.signal };
systemOne.mutate(variables);
}
@ -134,43 +170,37 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation =
<Select
value={endpoint}
onValueChange={(value) => {
if (value === "/v1/systemone" || value === "/typesafe/v1/systemone") {
if (value === "/v1/decisions" || value === "/v1/systemone") {
clearRequestState();
setChosenEndpoint(value);
setEndpoint(value);
}
}}
>
<SelectTrigger className="w-80" aria-label="Decision endpoint">
<SelectValue>
{endpoint === "/v1/systemone" ? "System One · /v1/systemone" : "TypeSafe · /typesafe/v1/systemone"}
</SelectValue>
<SelectValue>{endpoint}</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="/v1/systemone">System One · /v1/systemone</SelectItem>
<SelectItem value="/typesafe/v1/systemone">TypeSafe · /typesafe/v1/systemone</SelectItem>
<SelectItem value="/v1/decisions">/v1/decisions</SelectItem>
<SelectItem value="/v1/systemone">/v1/systemone</SelectItem>
</SelectContent>
</Select>
{endpoint === "/v1/systemone" && (
<>
<span className="ml-2 text-sm font-medium text-muted-foreground">Model</span>
<div className="w-80">
<SearchSelect
aria-label="Decision model"
options={decisionModels.map((model) => ({ label: model, value: model }))}
value={payloadModel(rawPayload) ?? null}
onValueChange={handleModelPick}
placeholder={decisionModels.length > 0 ? "Pick a decision model" : "No decision models yet"}
emptyText="No decision models found"
disabled={hasSyntaxError}
allowClear={false}
/>
</div>
{decisionModelsLoaded && decisionModels.length === 0 && (
<a href={uiHref("models-and-endpoints")} className="text-sm text-primary underline">
Add a decision model
</a>
)}
</>
<span className="ml-2 text-sm font-medium text-muted-foreground">Model</span>
<div className="w-80">
<SearchSelect
aria-label="Decision model"
options={decisionModels.map((model) => ({ label: model, value: model }))}
value={payloadModel(rawPayload) ?? null}
onValueChange={handleModelPick}
placeholder={decisionModels.length > 0 ? "Pick a decision model" : "No decision models yet"}
emptyText="No decision models found"
disabled={hasSyntaxError}
allowClear={false}
/>
</div>
{decisionModelsLoaded && decisionModels.length === 0 && (
<a href={uiHref("models-and-endpoints")} className="text-sm text-primary underline">
Add a decision model
</a>
)}
</div>
<div className="flex flex-wrap items-center gap-3">
@ -207,9 +237,14 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation =
)}
</div>
<div className="ml-auto flex items-center gap-2">
<Button variant="outline" onClick={handleResetExample} disabled={rawPayload === examplePayload}>
<RotateCcw />
Reset example
<PresetPicker activePreset={activePreset} onPick={handlePresetPick} />
<Button
variant="outline"
onClick={() => handlePayloadChange(clearedPayload)}
disabled={rawPayload === clearedPayload}
>
<Eraser />
Clear
</Button>
{view === "json" && (
<Button variant="outline" onClick={handleFormatJson} disabled={!rawPayload.trim() || hasSyntaxError}>
@ -230,13 +265,20 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation =
</div>
<Alert role="note" aria-label="Decision endpoint notice">
<Info />
<AlertTitle>
{endpoint === "/v1/systemone" ? "Decision models · System One" : "TypeSafe Jev · System One"}
</AlertTitle>
<AlertTitle>{endpoint}</AlertTitle>
<AlertDescription>
{endpoint === "/v1/systemone"
? "Sends choice, noul, and score questions through /v1/systemone to a decision model on your proxy. Pick one under Model, or omit model to use the proxy's configured default."
: "Sends requests through /typesafe/v1/systemone and requires TYPESAFE_API_KEY on the proxy."}{" "}
{endpoint === "/v1/decisions" ? (
"Uses input and a questions array with predicate, choice, and score questions. Edit the form or JSON and inspect the response JSON in the OpenAI Decisions format."
) : (
<>
Uses state and a questions object with{" "}
<SimpleTooltip content={NOUL_HELP} className="underline decoration-dotted underline-offset-2">
noul
</SimpleTooltip>
, choice, and score questions in the System One format.
</>
)}{" "}
Pick a decision model under Model, or omit model to use the proxy&apos;s configured default.{" "}
<a href={DECISIONS_DOCS_URL} target="_blank" rel="noopener noreferrer" className="underline">
How to call /v1/decisions and /v1/systemone
</a>
@ -249,7 +291,7 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation =
</section>
<div className="grid gap-4 xl:min-h-0 xl:flex-1 xl:grid-cols-2">
<section className="flex min-h-96 flex-col xl:min-h-0" aria-label="System One request editor">
<section className="flex min-h-96 flex-col xl:min-h-0" aria-label="Decisions request editor">
<Tabs
value={view}
onValueChange={(value) => (value === "form" || value === "json") && setView(value)}
@ -260,27 +302,45 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation =
<TabsTrigger value="json">JSON</TabsTrigger>
</TabsList>
<TabsContent value="form" className="flex min-h-0 flex-col">
<SystemOneForm
{validated.endpoint === "/v1/decisions" ? (
<OpenAIDecisionsForm
value={rawPayload}
onChange={handlePayloadChange}
validation={validated.validation}
onOpenJson={() => setView("json")}
/>
) : (
<SystemOneForm
value={rawPayload}
onChange={handlePayloadChange}
validation={validated.validation}
onOpenJson={() => setView("json")}
/>
)}
</TabsContent>
<TabsContent value="json" className="flex min-h-0 flex-col">
<JsonEditor
value={rawPayload}
onChange={handlePayloadChange}
validation={validation}
onOpenJson={() => setView("json")}
label={endpoint === "/v1/decisions" ? "Decisions JSON payload" : "System One JSON payload"}
placeholder={`Paste or write a ${endpoint} request`}
/>
</TabsContent>
<TabsContent value="json" className="flex min-h-0 flex-col">
<JsonEditor value={rawPayload} onChange={handlePayloadChange} validation={validation} />
</TabsContent>
</Tabs>
</section>
<section className="grid content-start gap-4 xl:min-h-0 xl:overflow-auto" aria-label="System One results">
<section className="grid content-start gap-4 xl:min-h-0 xl:overflow-auto" aria-label="Decisions results">
<ResponseView
response={systemOne.data?.response}
view={view}
fallbackModel={validation.payload?.model}
latencyMs={systemOne.data?.latencyMs}
error={systemOne.error?.message}
isLoading={isLoading}
/>
<QuestionBreakdown payload={validation.payload} />
{view === "form" && validated.endpoint === "/v1/systemone" && (
<QuestionBreakdown payload={validated.validation.payload} />
)}
</section>
</div>
</div>

View file

@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import { validateSystemOnePayload } from "./validatePayload";
import { validateSystemOnePayload, validateOpenAIDecisionsPayload } from "./validatePayload";
import { systemOneResponseSchema } from "./schemas";
import { openAIDecisionsExample } from "./example";
const request = {
model: "configured-decision-model",
@ -81,3 +82,53 @@ describe("native decisions validation", () => {
expect(systemOneResponseSchema.parse(response)).toEqual(response);
});
});
describe("/v1/decisions validation", () => {
const example = openAIDecisionsExample("jev-latest");
const validateOpenAI = (value: unknown) => validateOpenAIDecisionsPayload(JSON.stringify(value));
it("keeps input, question arrays, provider extensions and optional model omission intact", () => {
const payload = { input: example.input, questions: example.questions, provider_option: { enabled: true } };
expect(validateOpenAI(payload)).toEqual({ isValid: true, payload, issues: [] });
expect(validateOpenAI(example).isValid).toBe(true);
});
it("accepts multimodal messages and boolean choices without converting them to strings", () => {
const payload = {
input: [
{
role: "user",
content: [
{ type: "input_text", text: "Does this show an error?" },
{ type: "input_image", image_url: "data:image/png;base64,example" },
],
},
],
questions: [{ type: "choice", instructions: "Is this an error?", choices: [{ value: true }, { value: false }] }],
};
expect(validateOpenAI(payload)).toEqual({ isValid: true, payload, issues: [] });
});
it.each([
request,
{ ...example, questions: [] },
{ ...example, questions: request.questions },
{ ...example, input: {} },
{ ...example, questions: [{ type: "noul", instructions: "Test" }] },
{ ...example, questions: [{ type: "predicate" }] },
{ ...example, questions: [{ type: "predicate", instructions: "Test", criteria: {} }] },
{ ...example, questions: [{ type: "choice", instructions: "Test", choices: [{ value: true }] }] },
{
...example,
questions: [{ type: "choice", instructions: "Test", choices: [{ value: true }, { value: "true" }] }],
},
{ ...example, questions: [{ type: "score", instructions: "Test", levels: [{ label: "Low" }] }] },
])("rejects the wrong endpoint's shape and invalid question definitions %#", (payload) => {
expect(validateOpenAI(payload).isValid).toBe(false);
});
it("rejects blank and malformed JSON without throwing", () => {
expect(validateOpenAIDecisionsPayload(" ").issues[0]?.path).toBe("root");
expect(validateOpenAIDecisionsPayload("{").issues[0]?.path).toBe("syntax");
});
});

View file

@ -0,0 +1,60 @@
import { describe, expect, it } from "vitest";
import {
DECISION_PRESETS,
emptyPayload,
openAIDecisionsExample,
presetPayload,
toOpenAIDecisionsRequest,
matchPreset,
} from "./example";
import { validateOpenAIDecisionsPayload, validateSystemOnePayload } from "./validatePayload";
describe("decision presets", () => {
it.each(DECISION_PRESETS.map((preset) => [preset.label, preset] as const))(
"%s is a valid request on both endpoints",
(_label, preset) => {
expect(
validateSystemOnePayload(presetPayload(preset, "/v1/systemone", "jev-latest"), "/v1/systemone"),
).toMatchObject({ isValid: true, issues: [] });
expect(validateOpenAIDecisionsPayload(presetPayload(preset, "/v1/decisions", "jev-latest"))).toMatchObject({
isValid: true,
issues: [],
});
},
);
it("covers every question type across the presets", () => {
const types = new Set(
DECISION_PRESETS.flatMap((preset) => Object.values(preset.request.questions).map((question) => question.type)),
);
expect([...types].sort()).toEqual(["choice", "noul", "score"]);
});
it("maps System One questions onto the OpenAI shape by name", () => {
const request = toOpenAIDecisionsRequest(DECISION_PRESETS[0].request, "jev-latest");
expect(request.questions.map((question) => [question.type, question.name])).toEqual([
["choice", "area"],
["predicate", "has_repro_steps"],
["score", "severity"],
]);
expect(request).toEqual(openAIDecisionsExample("jev-latest"));
});
it("recognises a preset whatever the model key holds or where it sits", () => {
const lead = DECISION_PRESETS.find((preset) => preset.id === "lead-scoring");
const parsed = JSON.parse(presetPayload(lead!, "/v1/systemone", "jev-latest")) as Record<string, unknown>;
const { model: _model, ...rest } = parsed;
expect(matchPreset("/v1/systemone", JSON.stringify({ ...rest, model: "pplx-decider" }))).toBe(lead);
expect(matchPreset("/v1/systemone", JSON.stringify(rest))).toBe(lead);
expect(matchPreset("/v1/decisions", presetPayload(lead!, "/v1/decisions"))).toBe(lead);
expect(matchPreset("/v1/decisions", presetPayload(lead!, "/v1/systemone"))).toBeUndefined();
expect(matchPreset("/v1/systemone", JSON.stringify({ ...rest, state: "edited" }))).toBeUndefined();
expect(matchPreset("/v1/systemone", "{")).toBeUndefined();
});
it("omits the model from payloads when none is known", () => {
expect(JSON.parse(presetPayload(DECISION_PRESETS[1], "/v1/systemone"))).not.toHaveProperty("model");
expect(JSON.parse(emptyPayload("/v1/decisions"))).toEqual({ input: "", questions: [] });
expect(validateSystemOnePayload(emptyPayload("/v1/systemone"), "/v1/systemone").isValid).toBe(false);
});
});

View file

@ -1,4 +1,39 @@
import type { DecisionRequest, SystemOneRequest } from "./schemas";
import type { OpenAIDecisionsRequest } from "./openAIDecisions";
import { parseJson } from "./validatePayload";
type PresetChoice = {
type: "choice";
instructions: string;
criteria: Record<string, string>;
};
type PresetNoul = {
type: "noul";
instructions: string;
criteria?: { true: string; false: string };
};
type PresetScore = {
type: "score";
instructions: string;
criteria: string[];
};
export type PresetQuestion = PresetChoice | PresetNoul | PresetScore;
export type PresetRequest = {
state: string;
questions: Record<string, PresetQuestion>;
};
type PresetEndpoint = "/v1/decisions" | "/v1/systemone";
export interface DecisionPreset {
id: string;
label: string;
request: PresetRequest;
}
export const SYSTEM_ONE_EXAMPLE = {
model: "jev-latest",
@ -37,6 +72,132 @@ export const SYSTEM_ONE_EXAMPLE = {
},
} satisfies SystemOneRequest;
export const PLACEHOLDER_DECISION_MODEL = "your-decision-model";
const { model: _exampleModel, ...bugTriage }: SystemOneRequest & PresetRequest = SYSTEM_ONE_EXAMPLE;
export const decisionsExample = (model: string): DecisionRequest => ({ ...SYSTEM_ONE_EXAMPLE, model });
const supportRouting = {
state:
"Hi, I was charged twice for my team plan this month and the second charge pushed my card over its limit. I need the duplicate refunded today and want to know why it happened.",
questions: {
department: {
type: "choice",
instructions: "Which team should handle this ticket?",
criteria: {
billing: "Invoices, charges, refunds, and plan changes",
technical: "Bugs, errors, outages, and integration help",
account: "Login, permissions, and profile settings",
sales: "Pricing questions and new purchases",
},
},
},
} satisfies PresetRequest;
const moderation = {
state:
"This guide is garbage and whoever wrote it should be fired. The fix that actually works is setting stream=false, took me two days to figure that out.",
questions: {
breaks_rules: {
type: "noul",
instructions: "Does this comment break the community guidelines?",
criteria: {
true: "Insults, harassment, slurs, or threats aimed at a person",
false: "Blunt or frustrated but still about the content",
},
},
},
} satisfies PresetRequest;
const leadScoring = {
state:
"Inbound form: VP Engineering at a 400 person fintech. Running a self hosted gateway today, evaluating replacements this quarter, budget approved, needs SSO and audit logs.",
questions: {
fit: {
type: "score",
instructions: "How well does this lead fit our ideal customer?",
criteria: [
"No fit, wrong market or no budget",
"Weak fit, early curiosity only",
"Possible fit, needs discovery",
"Strong fit, active evaluation",
"Ideal fit, ready to buy this quarter",
],
},
},
} satisfies PresetRequest;
export const DECISION_PRESETS: readonly DecisionPreset[] = [
{ id: "bug-triage", label: "Triage a bug report", request: bugTriage },
{ id: "support-routing", label: "Route a support ticket", request: supportRouting },
{ id: "moderation", label: "Moderate a forum comment", request: moderation },
{ id: "lead-scoring", label: "Score a sales lead", request: leadScoring },
];
const toOpenAIQuestion = (name: string, question: PresetQuestion): OpenAIDecisionsRequest["questions"][number] => {
switch (question.type) {
case "choice":
return {
type: "choice",
name,
instructions: question.instructions,
choices: Object.entries(question.criteria).map(([value, description]) => ({ value, description })),
};
case "noul":
return { type: "predicate", name, instructions: question.instructions };
case "score":
return {
type: "score",
name,
instructions: question.instructions,
levels: question.criteria.map((description, index) => ({ label: String(index), description })),
};
}
};
export const toSystemOneRequest = (request: PresetRequest, model?: string): DecisionRequest => ({ model, ...request });
export const toOpenAIDecisionsRequest = (request: PresetRequest, model?: string): OpenAIDecisionsRequest => ({
model,
input: request.state,
questions: Object.entries(request.questions).map(([name, question]) => toOpenAIQuestion(name, question)),
});
export const presetPayload = (preset: DecisionPreset, endpoint: PresetEndpoint, model?: string): string =>
JSON.stringify(
endpoint === "/v1/decisions"
? toOpenAIDecisionsRequest(preset.request, model)
: toSystemOneRequest(preset.request, model),
null,
2,
);
const stableJson = (value: unknown): string =>
JSON.stringify(value, (_key, item: unknown) =>
item !== null && typeof item === "object" && !Array.isArray(item)
? Object.fromEntries(Object.entries(item as Record<string, unknown>).sort(([a], [b]) => a.localeCompare(b)))
: item,
);
const withoutModel = (value: unknown): string => {
const { model: _model, ...rest } = (value ?? {}) as Record<string, unknown>;
return stableJson(rest);
};
export const matchPreset = (endpoint: PresetEndpoint, raw: string): DecisionPreset | undefined => {
const json = parseJson(raw);
if (!json.ok) {
return undefined;
}
const current = withoutModel(json.value);
return DECISION_PRESETS.find((preset) => withoutModel(JSON.parse(presetPayload(preset, endpoint))) === current);
};
export const emptyPayload = (endpoint: PresetEndpoint, model?: string): string =>
JSON.stringify(
endpoint === "/v1/decisions" ? { model, input: "", questions: [] } : { model, state: "", questions: {} },
null,
2,
);
export const decisionsExample = (model?: string): DecisionRequest => toSystemOneRequest(bugTriage, model);
export const openAIDecisionsExample = (model?: string): OpenAIDecisionsRequest =>
toOpenAIDecisionsRequest(bugTriage, model);

Some files were not shown because too many files have changed in this diff Show more