litellm/tests/unit/systemone/test_main.py
Mateo Wang 3fec5075fa
feat(typesafe): support Jev 1.14 images, box questions, and long-context headers (#46596)
* feat(typesafe): support Jev 1.14 images, box questions, and long-context headers

* fix(cost): drop the typesafe/jev-1.14.0 entry main already prices

* fix(typesafe): send images only with the images header and box questions only to models that answer them

TypeSafe takes assets and input_image parts only when the request carries
TypeSafe-Danger-Zone-Images: true, so without it they follow drop_params like
any other provider that cannot take images. Box support now comes from a
supports_box_questions cost-map flag set on jev-1.14.0 and jev-preview, so
jev-1.13.0 and jev-latest refuse or drop a box question before the call. A
malformed provider_specific_header answers 400 instead of 500.

* fix(typesafe): allow null box confidence and validate asset references and base64

* fix(typesafe): gate box questions on the images header, drop unreadable assets under drop_params, forward opt-in headers on the pass-through

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-14 22:29:48 -07:00

488 lines
19 KiB
Python

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
from tests.unit.conftest import AsyncOnlyImageFetch
_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"
_CLOUDFLARE_API_BASE: Final = "https://api.cloudflare.com/client/v4/accounts/acct/ai/run"
_CLOUDFLARE_CLEF_URL: Final = f"{_CLOUDFLARE_API_BASE}/@cf/cloudflare/clef"
_PNG_DATA_URL: Final = "data:image/png;base64,AA=="
@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_decisions_with_state_still_sends_its_images(respx_mock: respx.MockRouter) -> None:
route: Final = respx_mock.post(_CLOUDFLARE_CLEF_URL).respond(json=_SYSTEMONE_RESPONSE)
with pytest.warns(UserWarning, match=r"litellm\.systemone"):
await litellm.adecisions(
model="cloudflare/clef",
state=_STATE,
questions=_SYSTEMONE_QUESTIONS,
images=[_PNG_DATA_URL],
api_key="caller-key",
api_base=_CLOUDFLARE_API_BASE,
)
sent: Final = json.loads(respx_mock.calls.last.request.content)
assert sent["images"] == [_PNG_DATA_URL]
assert route.called
@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)
_BOX_RESPONSE: Final[Mapping[str, object]] = {
"model": "jev-1.14.0",
"answers": {
"has_mug": {"type": "noul", "noul": 0.97},
"mug": {"type": "box", "box": {"top": 0.1, "bottom": 0.5, "left": 0.2, "right": 0.6}, "confidence": 0.84},
},
"usage": {"input_tokens": 299, "output_tokens": 37},
}
_MUG_BOX: Final[Mapping[str, float]] = MappingProxyType({"top": 0.1, "bottom": 0.5, "left": 0.2, "right": 0.6})
_IMAGES_HEADER: Final[Mapping[str, str]] = MappingProxyType({"TypeSafe-Danger-Zone-Images": "true"})
@pytest.mark.asyncio
async def test_systemone_sends_assets_and_box_questions_to_typesafe(respx_mock: respx.MockRouter) -> None:
route: Final = respx_mock.post(_TYPESAFE_URL).respond(json=_BOX_RESPONSE)
questions: Final[Mapping[str, object]] = {
"has_mug": {"type": "noul", "instructions": "Is there a mug in `photo-1`?"},
"mug": {"type": "box", "instructions": "the mug in `photo-1`"},
}
response: Final = await litellm.asystemone(
model="typesafe/jev-1.14.0",
state={"photo": "photo-1"},
assets={"photo-1": {"type": "image", "format": "png", "data_base64": "AA=="}},
questions=questions,
api_key="caller-key",
extra_headers=_IMAGES_HEADER,
)
assert json.loads(route.calls.last.request.content) == {
"model": "jev-1.14.0",
"state": {"photo": "photo-1"},
"questions": questions,
"assets": {"photo-1": {"type": "image", "format": "png", "data_base64": "AA=="}},
}
assert isinstance(response, DecisionsResponse)
assert response.answers["mug"].model_dump(mode="json") == {"type": "box", "box": _MUG_BOX, "confidence": 0.84}
@pytest.mark.asyncio
async def test_decisions_input_images_reach_typesafe_as_assets_referenced_in_the_state(
respx_mock: respx.MockRouter,
) -> None:
route: Final = respx_mock.post(_TYPESAFE_URL).respond(json=_BOX_RESPONSE)
response: Final = await litellm.adecisions(
model="typesafe/jev-1.14.0",
input=[
{
"role": "user",
"content": [
{"type": "input_text", "text": "Desk photo:"},
{"type": "input_image", "image_url": _PNG_DATA_URL},
],
}
],
questions=[
{"type": "predicate", "name": "has_mug", "instructions": "Is there a mug on the desk?"},
{"type": "box", "name": "mug", "instructions": "the mug on the desk"},
],
api_key="caller-key",
extra_headers=_IMAGES_HEADER,
)
sent: Final = json.loads(route.calls.last.request.content)
(reference,) = sent["assets"]
assert sent["state"] == f"Desk photo:\n\n{reference}"
assert sent["assets"][reference] == {"type": "image", "format": "png", "data_base64": "AA=="}
assert sent["questions"]["mug"] == {"type": "box", "instructions": "the mug on the desk"}
assert isinstance(response, OpenAIDecisionResponse)
assert [answer.model_dump(mode="json") for answer in response.answers] == [
{"type": "predicate", "name": "has_mug", "probability": 0.97},
{"type": "box", "name": "mug", "box": _MUG_BOX, "confidence": 0.84},
]
@pytest.mark.asyncio
async def test_decisions_remote_images_reach_typesafe_inlined_as_assets(
respx_mock: respx.MockRouter, async_only_image_fetch: AsyncOnlyImageFetch
) -> None:
route: Final = respx_mock.post(_TYPESAFE_URL).respond(json=_BOX_RESPONSE)
await litellm.adecisions(
model="typesafe/jev-1.14.0",
input=[{"role": "user", "content": [{"type": "input_image", "image_url": "https://images.example/desk.png"}]}],
questions=[{"type": "predicate", "name": "has_mug", "instructions": "Is there a mug on the desk?"}],
api_key="caller-key",
extra_headers=_IMAGES_HEADER,
)
sent: Final = json.loads(route.calls.last.request.content)
assert async_only_image_fetch.fetched == ["https://images.example/desk.png"]
assert sent["assets"] == {
sent["state"]: {"type": "image", "format": "png", "data_base64": async_only_image_fetch.base64_png}
}
@pytest.mark.asyncio
@pytest.mark.parametrize(
("scoped_provider", "expected_headers"),
(("typesafe", {"typesafe-danger-zone-long-context": "true"}), ("perplexity", {})),
ids=("typesafe", "another_provider"),
)
async def test_provider_specific_headers_reach_typesafe_only_when_scoped_to_it(
scoped_provider: str, expected_headers: Mapping[str, str], respx_mock: respx.MockRouter
) -> None:
route: Final = respx_mock.post(_TYPESAFE_URL).respond(json=_SYSTEMONE_RESPONSE)
await litellm.asystemone(
model="typesafe/jev-1.14.0",
state=_STATE,
questions=_SYSTEMONE_QUESTIONS,
api_key="caller-key",
provider_specific_header={
"custom_llm_provider": scoped_provider,
"extra_headers": {"TypeSafe-Danger-Zone-Long-Context": "true"},
},
)
sent_headers: Final = route.calls.last.request.headers
assert {
name: value for name, value in sent_headers.items() if name.startswith("typesafe-danger-zone-")
} == expected_headers
_DESK_PHOTO_INPUT: Final[tuple[Mapping[str, object], ...]] = (
{
"role": "user",
"content": [
{"type": "input_text", "text": "Desk photo:"},
{"type": "input_image", "image_url": _PNG_DATA_URL},
],
},
)
_HAS_MUG_QUESTION: Final[tuple[Mapping[str, object], ...]] = (
{"type": "predicate", "name": "has_mug", "instructions": "Is there a mug on the desk?"},
)
_WITHOUT_IMAGES_HEADER: Final[tuple[Mapping[str, str], ...]] = ({}, {"TypeSafe-Danger-Zone-Images": "false"})
@pytest.mark.parametrize("extra_headers", _WITHOUT_IMAGES_HEADER, ids=("no_header", "header_false"))
def test_typesafe_refuses_images_and_assets_before_http_without_the_images_header(
extra_headers: Mapping[str, str], respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(litellm, "drop_params", False)
route: Final = respx_mock.post(_TYPESAFE_URL).respond(json=_BOX_RESPONSE)
with pytest.raises(litellm.UnsupportedParamsError, match=r"\['input_image'\]") as image_refusal:
litellm.decisions(
model="typesafe/jev-1.14.0",
input=_DESK_PHOTO_INPUT,
questions=_HAS_MUG_QUESTION,
api_key="caller-key",
extra_headers=extra_headers,
)
with pytest.raises(litellm.UnsupportedParamsError, match=r"\['assets'\]") as asset_refusal:
litellm.systemone(
model="typesafe/jev-1.14.0",
state="review photo-1",
assets={"photo-1": {"format": "png", "data_base64": "AA=="}},
questions=_SYSTEMONE_QUESTIONS,
api_key="caller-key",
extra_headers=extra_headers,
)
assert (image_refusal.value.status_code, asset_refusal.value.status_code) == (400, 400)
assert not route.called
@pytest.mark.asyncio
async def test_typesafe_drops_images_and_assets_without_the_images_header_under_drop_params(
respx_mock: respx.MockRouter,
) -> None:
route: Final = respx_mock.post(_TYPESAFE_URL).respond(json=_BOX_RESPONSE)
await litellm.adecisions(
model="typesafe/jev-1.14.0",
input=_DESK_PHOTO_INPUT,
questions=_HAS_MUG_QUESTION,
api_key="caller-key",
drop_params=True,
)
decisions_body: Final = json.loads(route.calls.last.request.content)
await litellm.asystemone(
model="typesafe/jev-1.14.0",
state="review photo-1",
assets={"photo-1": {"format": "png", "data_base64": "AA=="}},
questions=_SYSTEMONE_QUESTIONS,
api_key="caller-key",
drop_params=True,
)
systemone_body: Final = json.loads(route.calls.last.request.content)
assert (decisions_body["state"], "assets" in decisions_body) == ("Desk photo:", False)
assert "assets" not in systemone_body
_BOX_UNSUPPORTED: Final[tuple[tuple[str, Mapping[str, str]], ...]] = (
("typesafe/jev-1.13.0", _IMAGES_HEADER),
("typesafe/jev-1.14.0", MappingProxyType({})),
)
_BOX_UNSUPPORTED_IDS: Final = ("model_without_box_support", "without_the_images_header")
@pytest.mark.parametrize(("model", "extra_headers"), _BOX_UNSUPPORTED, ids=_BOX_UNSUPPORTED_IDS)
def test_typesafe_refuses_box_questions_before_http_when_the_model_or_the_headers_do_not_allow_them(
model: str, extra_headers: Mapping[str, str], respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(litellm, "drop_params", False)
route: Final = respx_mock.post(_TYPESAFE_URL).respond(json=_SYSTEMONE_RESPONSE)
with pytest.raises(litellm.UnsupportedParamsError, match=r"\['box questions'\]") as caught:
litellm.systemone(
model=model,
state="a mug on a desk",
questions={"mug": {"type": "box", "instructions": "the mug"}, **_SYSTEMONE_QUESTIONS},
api_key="caller-key",
extra_headers=extra_headers,
)
assert caught.value.status_code == 400
assert not route.called
@pytest.mark.asyncio
@pytest.mark.parametrize(("model", "extra_headers"), _BOX_UNSUPPORTED, ids=_BOX_UNSUPPORTED_IDS)
async def test_typesafe_answers_box_questions_it_cannot_take_as_refusals_under_drop_params(
model: str, extra_headers: Mapping[str, str], respx_mock: respx.MockRouter
) -> None:
route: Final = respx_mock.post(_TYPESAFE_URL).respond(json=_SYSTEMONE_RESPONSE)
response: Final = await litellm.adecisions(
model=model,
input="Customer asked for a refund",
questions=(*_OPENAI_QUESTIONS, {"type": "box", "name": "mug", "instructions": "the mug"}),
api_key="caller-key",
extra_headers=extra_headers,
drop_params=True,
)
assert json.loads(route.calls.last.request.content)["questions"] == {
"is_refund": {"type": "noul", "instructions": "Is this a refund request?"}
}
assert isinstance(response, OpenAIDecisionResponse)
assert [answer.model_dump(mode="json") for answer in response.answers] == [
{"type": "predicate", "name": "is_refund", "probability": 0.93},
{"type": "refusal", "name": "mug"},
]
def test_a_malformed_provider_specific_header_is_ignored(respx_mock: respx.MockRouter) -> None:
route: Final = respx_mock.post(_TYPESAFE_URL).respond(json=_SYSTEMONE_RESPONSE)
response: Final = litellm.systemone(
model="typesafe/jev-1.14.0",
state=_STATE,
questions=_SYSTEMONE_QUESTIONS,
api_key="caller-key",
provider_specific_header={"custom_llm_provider": "typesafe", "extra_headers": "not a header map"},
)
assert isinstance(response, DecisionsResponse)
assert not [name for name in route.calls.last.request.headers if name.startswith("typesafe-danger-zone-")]
@pytest.mark.asyncio
async def test_url_safe_base64_images_reach_typesafe_as_standard_base64_assets(respx_mock: respx.MockRouter) -> None:
route: Final = respx_mock.post(_TYPESAFE_URL).respond(json=_BOX_RESPONSE)
await litellm.adecisions(
model="typesafe/jev-1.14.0",
input=[{"role": "user", "content": [{"type": "input_image", "image_url": "data:image/png;base64,-_8="}]}],
questions=_HAS_MUG_QUESTION,
api_key="caller-key",
extra_headers=_IMAGES_HEADER,
)
assert list(json.loads(route.calls.last.request.content)["assets"].values()) == [
{"type": "image", "format": "png", "data_base64": "+/8="}
]
def test_an_image_whose_data_url_is_not_base64_answers_400_before_typesafe_is_called(
respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(litellm, "drop_params", False)
route: Final = respx_mock.post(_TYPESAFE_URL).respond(json=_BOX_RESPONSE)
with pytest.raises(litellm.BadRequestError, match="input_image") as caught:
litellm.decisions(
model="typesafe/jev-1.14.0",
input=[
{"role": "user", "content": [{"type": "input_image", "image_url": "data:image/png;base64,not base64!"}]}
],
questions=_HAS_MUG_QUESTION,
api_key="caller-key",
extra_headers=_IMAGES_HEADER,
)
assert caught.value.status_code == 400
assert not route.called
@pytest.mark.asyncio
async def test_malformed_assets_are_dropped_and_well_formed_ones_kept_under_drop_params(
respx_mock: respx.MockRouter,
) -> None:
route: Final = respx_mock.post(_TYPESAFE_URL).respond(json=_SYSTEMONE_RESPONSE)
await litellm.asystemone(
model="typesafe/jev-1.14.0",
state="compare photo-1 with photo-2",
assets={
"photo-1": {"type": "image", "format": "png", "data_base64": "AA=="},
"photo-2": {"type": "image", "part": "file1"},
},
questions=_SYSTEMONE_QUESTIONS,
api_key="caller-key",
extra_headers=_IMAGES_HEADER,
drop_params=True,
)
assert json.loads(route.calls.last.request.content)["assets"] == {
"photo-1": {"type": "image", "format": "png", "data_base64": "AA=="}
}