fix(decisions): read a deployment's own supports_box_questions flag before the cost map (#46696)

* fix(decisions): read a deployment's own supports_box_questions flag before the cost map

The box-question check only read the shared cost-map row and required a decisions mode there, so an OpenAI-format deployment whose model_info sets supports_box_questions was refused with a 400, which kept the wire test from #46633 red on main

* fix(decisions): keep the box flag on its deployment and out of request bodies

A model_info that is not a plain dict no longer fails the call, the proxy
decisions endpoint drops a caller-sent model_info, and the router stops
copying supports_box_questions onto the shared cost-map key.

* fix(decisions): keep the box flag shared with same-model deployments like main

Scoping supports_box_questions to one deployment turned box questions on an unflagged sibling of the same backend model from reaching the provider into a 400. Siblings inherit the flag through the shared cost-map row again, as they do on main. Also formats two new tests.

* test(decisions): pin box flag inheritance by an unflagged same-model deployment

* test(decisions): audit box-question deployment flag across providers, routes, and chaos cases
This commit is contained in:
Mateo Wang 2026-10-15 12:34:36 -07:00 • committed by GitHub
parent fac0135d82
commit 1938f537bf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 1118 additions and 13 deletions

View file

@ -5,7 +5,7 @@ from types import MappingProxyType
from typing import Final, Literal, TypeAlias
import httpx
from pydantic import ConfigDict, TypeAdapter, ValidationError
from pydantic import BaseModel, ConfigDict, StrictBool, TypeAdapter, ValidationError
from typing_extensions import assert_never
import litellm
@ -290,6 +290,7 @@ def _unsupported_params(
model: str,
custom_llm_provider: str,
headers: Mapping[str, str],
supports_box_questions: bool,
) -> tuple[str, ...]:
refuses_safety_identifier: Final = (
ir_request.safety_identifier is not None and not provider_config.supports_safety_identifier
@ -298,9 +299,7 @@ def _unsupported_params(
None if provider_config.supports_images_in(ir_request.input, headers) else image_param(ir_request.input)
)
refuses_assets: Final = bool(ir_request.assets) and not provider_config.supports_assets(headers)
refuses_box_questions: Final = _has_box_question(ir_request.questions) and not (
provider_config.supports_box_questions(model, custom_llm_provider, headers)
)
refuses_box_questions: Final = _has_box_question(ir_request.questions) and not supports_box_questions
refuses_audio: Final = bool(ir_request.audio) and not provider_config.supports_audio_input(
model, custom_llm_provider
)
@ -328,9 +327,8 @@ def _is_not_box_question(question: DecisionsIRQuestion) -> bool:
def _without_unsupported_params(
ir_request: DecisionsIRRequest,
provider_config: BaseDecisionsConfig,
model: str,
custom_llm_provider: str,
headers: Mapping[str, str],
supports_box_questions: bool,
) -> DecisionsIRRequest:
return replace(
ir_request,
@ -343,7 +341,7 @@ def _without_unsupported_params(
assets=ir_request.assets if provider_config.supports_assets(headers) else MappingProxyType({}),
questions=(
ir_request.questions
if provider_config.supports_box_questions(model, custom_llm_provider, headers)
if supports_box_questions
else tuple(filter(_is_not_box_question, ir_request.questions))
),
)
@ -365,6 +363,19 @@ def _without_malformed_images(
return well_formed
class _DeploymentBoxQuestions(BaseModel):
model_config = ConfigDict(frozen=True, from_attributes=True)
supports_box_questions: StrictBool | None = None
def _deployment_supports_box_questions(kwargs: Mapping[str, object]) -> bool | None:
try:
return _DeploymentBoxQuestions.model_validate(kwargs.get("model_info") or {}).supports_box_questions
except ValidationError:
return None
def _provider_ir_request(
ir_request: DecisionsIRRequest,
*,
@ -374,12 +385,17 @@ def _provider_ir_request(
headers: Mapping[str, str],
kwargs: Mapping[str, object],
) -> DecisionsIRRequest | _UnsupportedParams | _NothingLeftAfterDrop:
unsupported: Final = _unsupported_params(ir_request, provider_config, model, custom_llm_provider, headers)
supports_box_questions: Final = provider_config.supports_box_questions(
model, custom_llm_provider, headers, _deployment_supports_box_questions(kwargs)
)
unsupported: Final = _unsupported_params(
ir_request, provider_config, model, custom_llm_provider, headers, supports_box_questions
)
if not unsupported:
return ir_request
if not _drops_params(kwargs):
return _UnsupportedParams(names=unsupported)
supported: Final = _without_unsupported_params(ir_request, provider_config, model, custom_llm_provider, headers)
supported: Final = _without_unsupported_params(ir_request, provider_config, headers, supports_box_questions)
if _leaves_no_input(supported.input):
return _NothingLeftAfterDrop(names=unsupported, missing="input to decide on")
if not supported.questions:

View file

@ -537,7 +537,11 @@ class BaseDecisionsConfig(ABC):
def supports_assets(self, headers: Mapping[str, str]) -> bool:
return False
def supports_box_questions(self, model: str, custom_llm_provider: str, headers: Mapping[str, str]) -> bool:
def supports_box_questions(
self, model: str, custom_llm_provider: str, headers: Mapping[str, str], deployment_flag: bool | None
) -> bool:
if deployment_flag is not None:
return deployment_flag
return _is_decisions_model(model, custom_llm_provider) and litellm.supports_box_questions(
model=model, custom_llm_provider=custom_llm_provider
)

View file

@ -94,8 +94,12 @@ class TypeSafeDecisionsConfig(BaseDecisionsConfig):
name.lower() == _IMAGES_HEADER and value.strip().lower() == "true" for name, value in headers.items()
)
def supports_box_questions(self, model: str, custom_llm_provider: str, headers: Mapping[str, str]) -> bool:
return self.supports_assets(headers) and super().supports_box_questions(model, custom_llm_provider, headers)
def supports_box_questions(
self, model: str, custom_llm_provider: str, headers: Mapping[str, str], deployment_flag: bool | None
) -> bool:
return self.supports_assets(headers) and super().supports_box_questions(
model, custom_llm_provider, headers, deployment_flag
)
def transform_decisions_request(
self,

View file

@ -99,12 +99,16 @@ def _fields_checked_before_routing(data: Mapping[str, object]) -> Mapping[str, o
return MappingProxyType({key: value for key, value in data.items() if key not in _FIELDS_CHECKED_AFTER_ROUTING})
_DEPLOYMENT_ONLY_FIELDS: Final = frozenset({"model_info"})
async def _request_data(request: Request, user_api_key_dict: UserAPIKeyAuth) -> dict[str, object]:
body: Final = await request.body()
try:
return _REQUEST_DATA_ADAPTER.validate_json(body)
data: Final = _REQUEST_DATA_ADAPTER.validate_json(body)
except ValidationError as error:
raise await _invalid_request(raw_data={}, error=error, user_api_key_dict=user_api_key_dict)
return {key: value for key, value in data.items() if key not in _DEPLOYMENT_ONLY_FIELDS}
async def _process_decisions(

View file

@ -0,0 +1,302 @@
import asyncio
import json
import threading
import uuid
from collections.abc import Mapping
from dataclasses import dataclass
from pathlib import Path
from queue import SimpleQueue
from types import MappingProxyType
from typing import Final
from urllib.parse import urlsplit
import httpx
import psutil
import pytest
import yaml
from integration._support.client import Gateway, eventually
from integration._support.database import read_rows
from integration._support.process import graceful_stop_seconds, owned_proxy_process
from integration._support.wire import Reply, Request, Wire, wire_server
from pydantic import JsonValue, TypeAdapter
_API_KEY: Final = "synthetic-decisions-key"
_JSON_OBJECT: Final = TypeAdapter(dict[str, JsonValue])
_BOX: Final[dict[str, JsonValue]] = {"top": 0.1, "bottom": 0.4, "left": 0.2, "right": 0.6}
_BOX_QUESTION: Final[dict[str, JsonValue]] = {"type": "box", "name": "crack", "instructions": "Where is the crack?"}
_SYSTEM_ONE_BOX_QUESTION: Final[dict[str, JsonValue]] = {"type": "box", "instructions": "Where is the crack?"}
_SYSTEM_ONE_BOX_ANSWER: Final[dict[str, JsonValue]] = {"type": "box", "box": _BOX, "confidence": 0.7}
_BOX_ANSWER: Final[dict[str, JsonValue]] = {"type": "box", "name": "crack", "box": _BOX, "confidence": 0.7}
_REFUSED_BOX: Final = "does not support parameters: ['box questions']"
_OPENAI_FORMAT_ROUTES: Final = ("/v1/decisions", "/decisions")
_ROUTES: Final = (*_OPENAI_FORMAT_ROUTES, "/v1/systemone", "/systemone")
_FLAGS: Final = ("true", "false", "unset")
@dataclass(frozen=True, slots=True)
class _Call:
route: str
model: str
marker: str
answers: bool
@dataclass(frozen=True, slots=True)
class _Served:
call: _Call
status: int
call_id: str
text: str
def _marker_of(request: Request) -> str:
state: Final = _JSON_OBJECT.validate_json(request.body)["state"]
assert isinstance(state, str), request.body
return state
def _reply(request: Request) -> Reply:
marker: Final = _marker_of(request)
answer: Final = {
"model": f"model-{marker}",
"answers": {"crack": _SYSTEM_ONE_BOX_ANSWER},
"usage": {"input_tokens": 12, "output_tokens": 1},
}
return Reply(body=json.dumps(answer).encode())
def _body(call: _Call) -> dict[str, JsonValue]:
if call.route in _OPENAI_FORMAT_ROUTES:
return {"model": call.model, "input": call.marker, "questions": [_BOX_QUESTION], "num_retries": 0}
return {
"model": call.model,
"state": call.marker,
"questions": {"crack": _SYSTEM_ONE_BOX_QUESTION},
"num_retries": 0,
}
async def _send(client: httpx.AsyncClient, key: str, call: _Call) -> _Served:
response: Final = await client.post(call.route, json=_body(call), headers={"Authorization": f"Bearer {key}"})
return _Served(call, response.status_code, response.headers.get("x-litellm-call-id", ""), response.text)
async def _burst(
base_url: str, key: str, calls: tuple[_Call, ...], *, tolerate_transport_errors: bool = False
) -> tuple[_Served, ...]:
async with httpx.AsyncClient(base_url=base_url, timeout=60, trust_env=False) as client:
results: Final = await asyncio.gather(
*(_send(client, key, call) for call in calls), return_exceptions=tolerate_transport_errors
)
assert all(not isinstance(result, BaseException) or isinstance(result, httpx.TransportError) for result in results)
return tuple(result for result in results if isinstance(result, _Served))
def _assert_served_its_own(served: _Served) -> None:
if not served.call.answers:
assert served.status == 400, (served.status, served.text)
assert _REFUSED_BOX in served.text, served.text
return
assert served.status == 200, (served.status, served.text)
body: Final = _JSON_OBJECT.validate_json(served.text)
assert body["model"] == f"model-{served.call.marker}", served.text
expected: Final[JsonValue] = (
[_BOX_ANSWER] if served.call.route in _OPENAI_FORMAT_ROUTES else {"crack": _SYSTEM_ONE_BOX_ANSWER}
)
assert body["answers"] == expected, served.text
def _statuses_by_call_id(call_ids: tuple[str, ...]) -> dict[str, JsonValue]:
placeholders: Final = ", ".join("%s" for _ in call_ids)
rows: Final = eventually(
lambda: read_rows(
f'SELECT request_id, status FROM "LiteLLM_SpendLogs" WHERE request_id IN ({placeholders})', call_ids
),
lambda found: len(found) >= len(call_ids),
seconds=70,
)
assert len(rows) == len(call_ids), rows
return {str(row["request_id"]): row["status"] for row in rows}
def _answered(served: tuple[_Served, ...]) -> tuple[_Served, ...]:
return tuple(item for item in served if item.call.answers)
def _assert_each_served_its_own_and_logged_once(served: tuple[_Served, ...]) -> None:
for item in served:
_assert_served_its_own(item)
answered: Final = _answered(served)
assert len({item.call_id for item in answered}) == len(answered), answered
assert set(_statuses_by_call_id(tuple(item.call_id for item in answered)).values()) == {"success"}
def _backend() -> str:
return f"perplexity/box-{uuid.uuid4().hex[:12]}"
def _calls(models: Mapping[str, str], count: int) -> tuple[_Call, ...]:
return tuple(
_Call(
route=_ROUTES[index % len(_ROUTES)],
model=models[_FLAGS[(index // len(_ROUTES)) % len(_FLAGS)]],
marker=f"{_FLAGS[(index // len(_ROUTES)) % len(_FLAGS)]}-{uuid.uuid4().hex}",
answers=_FLAGS[(index // len(_ROUTES)) % len(_FLAGS)] == "true",
)
for index in range(count)
)
@pytest.mark.timeout(180)
async def test_a_burst_over_every_route_and_flag_sends_boxes_only_for_flagged_deployments_and_logs_each_once(
gateway: Gateway,
) -> None:
with wire_server(_reply) as wire, gateway.scenario() as scenario:
flags: Final[Mapping[str, Mapping[str, JsonValue] | None]] = MappingProxyType(
{"true": {"supports_box_questions": True}, "false": {"supports_box_questions": False}, "unset": None}
)
models: Final = MappingProxyType(
{
flag: scenario.model(model=_backend(), api_base=wire.url, api_key=_API_KEY, model_info=model_info)
for flag, model_info in flags.items()
}
)
calls: Final = _calls(models, 36)
served: Final = await _burst(str(gateway.client.base_url), gateway.key, calls)
assert len(served) == 36
for item in served:
_assert_served_its_own(item)
answered: Final = _answered(served)
assert len(answered) == 12
assert len({item.call_id for item in answered}) == 12
assert set(_statuses_by_call_id(tuple(item.call_id for item in answered)).values()) == {"success"}
received: Final = wire.drain()
assert sorted(_marker_of(request) for request in received) == sorted(item.call.marker for item in answered)
assert {request.target for request in received} == {"/v1/decisions"}, received
@pytest.mark.timeout(180)
async def test_a_flagged_deployment_recovers_box_answers_after_an_upstream_outage_on_the_same_port(
gateway: Gateway,
) -> None:
base_url: Final = str(gateway.client.base_url)
with gateway.scenario() as scenario:
with wire_server(_reply) as wire:
port: Final = urlsplit(wire.url).port
assert port is not None
model: Final = scenario.model(
model=_backend(), api_base=wire.url, api_key=_API_KEY, model_info={"supports_box_questions": True}
)
before: Final = await _burst(base_url, gateway.key, _calls(MappingProxyType({"true": model}), 4))
during: Final = await _burst(base_url, gateway.key, _calls(MappingProxyType({"true": model}), 4))
assert [item.status for item in during] == [500] * 4, [item.text for item in during]
with wire_server(_reply, port=port) as recovered:
after: Final = await _burst(base_url, gateway.key, _calls(MappingProxyType({"true": model}), 4))
assert sorted(map(_marker_of, recovered.drain())) == sorted(item.call.marker for item in after)
for item in (*before, *after):
_assert_served_its_own(item)
statuses: Final = _statuses_by_call_id(tuple(item.call_id for item in (*before, *during, *after)))
assert statuses == {
**{item.call_id: "success" for item in (*before, *after)},
**{item.call_id: "failure" for item in during},
}
_YAML_FLAGS: Final[tuple[tuple[str, str, Mapping[str, JsonValue] | None, bool], ...]] = (
("box-yaml-true", "own", {"supports_box_questions": True}, True),
("box-yaml-false-on-true-row", "shared", {"supports_box_questions": False}, False),
("box-yaml-true-row", "shared", {"supports_box_questions": True, "mode": "evaluation"}, True),
("box-yaml-unset-on-true-row", "shared", None, True),
("box-yaml-string", "string", {"supports_box_questions": "true"}, False),
)
def _yaml_config(wire: Wire, tmp_path: Path, backends: Mapping[str, str]) -> Path:
base: Final = _JSON_OBJECT.validate_python(yaml.safe_load(Path("tests/integration/proxy_config.yaml").read_text()))
model_list: Final = [
{
"model_name": name,
"litellm_params": {"model": backends[backend], "api_base": wire.url, "api_key": _API_KEY},
**({} if model_info is None else {"model_info": dict(model_info)}),
}
for name, backend, model_info, _ in _YAML_FLAGS
]
path: Final = tmp_path / "decisions-box-flags.yaml"
path.write_text(yaml.safe_dump({**base, "model_list": model_list}))
return path
def _yaml_calls(per_model: int) -> tuple[_Call, ...]:
return tuple(
_Call(
route=_ROUTES[index % len(_ROUTES)],
model=_YAML_FLAGS[index % len(_YAML_FLAGS)][0],
marker=f"{_YAML_FLAGS[index % len(_YAML_FLAGS)][0]}-{uuid.uuid4().hex}",
answers=_YAML_FLAGS[index % len(_YAML_FLAGS)][3],
)
for index in range(per_model * len(_YAML_FLAGS))
)
def _open_upstream_connections(pid: int, upstream: str) -> int:
port: Final = urlsplit(upstream).port
return sum(
1
for connection in psutil.Process(pid).net_connections(kind="tcp")
if connection.status == psutil.CONN_ESTABLISHED and connection.raddr and connection.raddr.port == port
)
@pytest.mark.timeout(2 * graceful_stop_seconds() + 120)
async def test_yaml_box_flags_survive_a_worker_sigkill_mid_burst_and_hold_on_respawned_workers(
gateway: Gateway, tmp_path: Path
) -> None:
release: Final = threading.Event()
held_markers: Final[SimpleQueue[str]] = SimpleQueue()
backends: Final = MappingProxyType({"own": _backend(), "shared": _backend(), "string": _backend()})
def held(request: Request) -> Reply:
marker: Final = _marker_of(request)
if marker.startswith("held-"):
held_markers.put(marker)
assert release.wait(timeout=60), "The burst was never released"
return _reply(request)
with wire_server(held) as wire:
config: Final = _yaml_config(wire, tmp_path, backends)
with owned_proxy_process(gateway, tmp_path, {}, config=config, workers=2) as owned:
base_url: Final = str(owned.gateway.client.base_url)
key: Final = owned.gateway.key
first: Final = await _burst(base_url, key, _yaml_calls(2))
_assert_each_served_its_own_and_logged_once(first)
workers: Final = owned.worker_pids()
assert len(workers) == 2, workers
calls: Final = tuple(
_Call(
route=_ROUTES[index % len(_ROUTES)],
model="box-yaml-true",
marker=f"held-{uuid.uuid4().hex}",
answers=True,
)
for index in range(20)
)
burst: Final = asyncio.create_task(_burst(base_url, key, calls, tolerate_transport_errors=True))
await asyncio.to_thread(eventually, held_markers.qsize, lambda size: size == 20, 60)
held_by: Final = MappingProxyType({pid: _open_upstream_connections(pid, wire.url) for pid in workers})
assert sum(held_by.values()) == 20, held_by
victim_pid, survivor_pid = sorted(workers, key=held_by.__getitem__)
with owned.replacing_worker(victim_pid) as kill:
kill.freeze()
await asyncio.to_thread(kill)
release.set()
survived: Final = await burst
assert len(survived) == held_by[survivor_pid], (held_by, len(survived))
await asyncio.to_thread(_assert_each_served_its_own_and_logged_once, survived)
await asyncio.to_thread(owned.kill_worker, survivor_pid)
assert not workers & owned.worker_pids(), (workers, owned.worker_pids())
respawned: Final = await _burst(base_url, key, _yaml_calls(4))
await asyncio.to_thread(_assert_each_served_its_own_and_logged_once, respawned)
received: Final = tuple(map(_marker_of, wire.drain()))
assert sorted(received) == sorted(
(*(call.marker for call in calls), *(item.call.marker for item in _answered((*first, *respawned))))
)

View file

@ -0,0 +1,607 @@
import json
import uuid
from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field
from pathlib import Path
from queue import SimpleQueue
from types import MappingProxyType
from typing import Final, Literal
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 configless_proxy_process, graceful_stop_seconds
from integration._support.wire import Reply, Request, Wire, wire_server
from pydantic import JsonValue, TypeAdapter
import litellm
from litellm.constants import PROXY_CONFIG_RELOAD_INTERVAL_SECONDS
_API_KEY: Final = "synthetic-decisions-key"
_IMAGES_HEADER: Final = "TypeSafe-Danger-Zone-Images"
_JSON_OBJECT: Final = TypeAdapter(dict[str, JsonValue])
_JSON_LIST: Final = TypeAdapter(list[JsonValue])
_INPUT: Final = "Photo description: a phone screen with a diagonal crack from the top-left corner to the middle."
_BOX: Final[dict[str, JsonValue]] = {"top": 0.1, "bottom": 0.4, "left": 0.2, "right": 0.6}
_PREDICATE: Final[dict[str, JsonValue]] = {
"type": "predicate",
"name": "cracked",
"instructions": "Is the screen cracked?",
}
_BOX_QUESTION: Final[dict[str, JsonValue]] = {"type": "box", "name": "crack", "instructions": "Where is the crack?"}
_QUESTIONS: Final[tuple[dict[str, JsonValue], ...]] = (_PREDICATE, _BOX_QUESTION)
_PREDICATE_ANSWER: Final[dict[str, JsonValue]] = {"type": "predicate", "name": "cracked", "probability": 0.9}
_BOX_ANSWER: Final[dict[str, JsonValue]] = {"type": "box", "name": "crack", "box": _BOX, "confidence": 0.7}
_BOX_REFUSAL: Final[dict[str, JsonValue]] = {"type": "refusal", "name": "crack"}
_SYSTEM_ONE_QUESTIONS: Final[dict[str, JsonValue]] = {
"cracked": {"type": "noul", "instructions": "Is the screen cracked?"},
"crack": {"type": "box", "instructions": "Where is the crack?"},
}
_SYSTEM_ONE_ANSWERS: Final[dict[str, JsonValue]] = {
"cracked": {"type": "noul", "noul": 0.9},
"crack": {"type": "box", "box": _BOX, "confidence": 0.7},
}
_USAGE: Final[dict[str, JsonValue]] = {"input_tokens": 41, "output_tokens": 2}
_OPENAI_USAGE: Final[dict[str, JsonValue]] = {
"input_tokens": 41,
"input_tokens_details": {"cached_tokens": 0, "cache_write_tokens": 0},
"output_tokens": 2,
"output_tokens_details": {"reasoning_tokens": 0},
"total_tokens": 43,
}
_REFUSED_BOX: Final = "does not support parameters: ['box questions']"
_SPEND_QUERY: Final = (
'SELECT status, call_type, custom_llm_provider, model_group, model_id FROM "LiteLLM_SpendLogs" '
"WHERE request_id = %s"
)
_OPENAI_FORMAT_ROUTES: Final = ("/v1/decisions", "/decisions")
_SYSTEM_ONE_ROUTES: Final = ("/v1/systemone", "/systemone")
Envelope = Literal["plain", "result", "ai_decide"]
def _systemone_question(question: Mapping[str, JsonValue]) -> dict[str, JsonValue]:
kind: Final = "box" if question["type"] == "box" else "noul"
return {"type": kind, "instructions": question["instructions"]}
def _systemone_answer(question: JsonValue, *, probability_key: str) -> JsonValue:
if object_value(question)["type"] == "box":
return {"type": "box", "box": _BOX, "confidence": 0.7}
return {"type": "noul", probability_key: 0.9}
def _openai_answer(question: JsonValue) -> JsonValue:
fields: Final = object_value(question)
template: Final = _BOX_ANSWER if fields["type"] == "box" else _PREDICATE_ANSWER
return {**template, "name": fields["name"]}
def _expected_answers(questions: Sequence[Mapping[str, JsonValue]]) -> list[JsonValue]:
return [_openai_answer(dict(question)) for question in questions]
@dataclass(frozen=True, slots=True)
class _Member:
provider: str
model: str
path: str
body_model: str | None
api_key: str | None = _API_KEY
speaks_openai: bool = False
envelope: Envelope = "plain"
headers: Mapping[str, str] = field(default_factory=lambda: MappingProxyType({}))
def deployment(
self, scenario: Scenario, api_base: str, model_info: Mapping[str, JsonValue] | None, **extra: JsonValue
) -> str:
header_params: Final[dict[str, JsonValue]] = {"extra_headers": dict(self.headers)} if self.headers else {}
return scenario.model(
model=self.model,
api_base=api_base,
api_key=self.api_key,
model_info=model_info,
**header_params,
**extra,
)
def upstream_body(self, questions: Sequence[Mapping[str, JsonValue]]) -> dict[str, JsonValue]:
if self.speaks_openai:
return {"model": self.body_model, "input": _INPUT, "questions": [dict(question) for question in questions]}
keyed: Final[dict[str, JsonValue]] = {
string_value(question["name"]): _systemone_question(question) for question in questions
}
model_field: Final[dict[str, JsonValue]] = {} if self.body_model is None else {"model": self.body_model}
return {**model_field, "state": _INPUT, "questions": keyed}
def reply(self, request: Request) -> Reply:
body: Final = _JSON_OBJECT.validate_json(request.body)
if self.speaks_openai:
questions: Final = _JSON_LIST.validate_python(body["questions"])
answer: Final = {
"model": self.body_model,
"answers": list(map(_openai_answer, questions)),
"usage": _OPENAI_USAGE,
}
return Reply(body=json.dumps(answer).encode())
probability_key: Final = "probability" if self.envelope == "ai_decide" else "noul"
answers: Final = {
key: _systemone_answer(question, probability_key=probability_key)
for key, question in object_value(body["questions"]).items()
}
return Reply(body=json.dumps(self._enveloped(answers)).encode())
def _enveloped(self, answers: Mapping[str, JsonValue]) -> dict[str, JsonValue]:
plain: Final[dict[str, JsonValue]] = {"model": self.body_model, "answers": dict(answers), "usage": _USAGE}
if self.envelope == "result":
return {"result": plain, "success": True}
if self.envelope == "ai_decide":
return {"response": {"answers": dict(answers)}, "metadata": {"version": "1.0"}}
return plain
def _members(tag: str) -> Mapping[str, _Member]:
name: Final = f"box-{tag}"
return MappingProxyType(
{
"perplexity": _Member("perplexity", f"perplexity/{name}", "/v1/decisions", name),
"typesafe": _Member(
"typesafe",
f"typesafe/{name}",
"/v1/systemone",
name,
headers=MappingProxyType({_IMAGES_HEADER: "true"}),
),
"openrouter": _Member("openrouter", f"openrouter/typesafe/{name}", "/alpha/decisions", f"typesafe/{name}"),
"fireworks_ai": _Member(
"fireworks_ai",
f"fireworks_ai/accounts/fireworks/routers/{name}",
"/v1/systemone",
f"accounts/fireworks/routers/{name}",
),
"cloudflare": _Member(
"cloudflare", f"cloudflare/{name}", f"/ai/run/@cf/cloudflare/{name}", name, envelope="result"
),
"strands_decider": _Member("strands_decider", f"strands_decider/{name}", "/v1/systemone", name, None),
"databricks": _Member("databricks", f"databricks/{name}", f"/{name}/invocations", name),
"databricks_ai_decide": _Member(
"databricks", "databricks/ai_decide", "/api/2.0/ai-functions/ai-decide", None, envelope="ai_decide"
),
"hosted_vllm": _Member("hosted_vllm", f"hosted_vllm/{name}", "/v1/systemone", name, None),
"laya": _Member("laya", "laya/typed-decisions", "/v1/systemone", "typed-decisions", None),
"bespoke": _Member(
"bespoke",
"bespoke/bespokelabs/Bespoke-Nimble-9B",
"/v1/systemone",
"bespokelabs/Bespoke-Nimble-9B",
None,
),
"azure_ai": _Member("azure_ai", f"azure_ai/{name}", "/providers/microsoft/v1/systemone", name),
"openai": _Member("openai", f"openai/{name}", "/v1/decisions", name, speaks_openai=True),
}
)
_MEMBER_LABELS: Final = tuple(_members("labels"))
def _tag() -> str:
return uuid.uuid4().hex[:12]
def _perplexity() -> _Member:
return _members(_tag())["perplexity"]
def _decide(
gateway: Gateway,
model: str,
questions: Sequence[Mapping[str, JsonValue]] = _QUESTIONS,
*,
route: str = "/v1/decisions",
**extra: JsonValue,
) -> httpx.Response:
body: Final[dict[str, JsonValue]] = {
"model": model,
"input": _INPUT,
"questions": [dict(question) for question in questions],
"num_retries": 0,
**extra,
}
return gateway.request("POST", route, body)
def _decide_in_system_one_format(gateway: Gateway, model: str, route: str, **extra: JsonValue) -> httpx.Response:
body: Final[dict[str, JsonValue]] = {
"model": model,
"state": _INPUT,
"questions": _SYSTEM_ONE_QUESTIONS,
"num_retries": 0,
**extra,
}
return gateway.request("POST", route, body)
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_box(response: httpx.Response) -> None:
assert response.status_code == 400, response.text
assert _REFUSED_BOX in response.text, response.text
def _authorization(member: _Member) -> str:
return f"Bearer {member.api_key}" if member.api_key else ""
@pytest.mark.parametrize("label", _MEMBER_LABELS)
def test_a_flagged_deployment_of_every_decisions_provider_sends_box_questions_and_returns_the_box(
gateway: Gateway, label: str
) -> None:
member: Final = _members(_tag())[label]
with wire_server(member.reply) as wire, gateway.scenario() as scenario:
model: Final = member.deployment(scenario, wire.url, {"supports_box_questions": True})
response: Final = _decide(gateway, model)
assert response.status_code == 200, response.text
assert response.json()["answers"] == _expected_answers(_QUESTIONS), response.text
(call,) = wire.drain()
assert (call.method, call.target) == ("POST", member.path)
assert call.headers.get("authorization", "") == _authorization(member)
assert _JSON_OBJECT.validate_json(call.body) == member.upstream_body(_QUESTIONS)
assert all(call.headers.get(name.lower()) == value for name, value in member.headers.items()), call.headers
row: Final = _spend_row(response.headers["x-litellm-call-id"])
assert (row["status"], row["call_type"], row["custom_llm_provider"], row["model_group"]) == (
"success",
"adecisions",
member.provider,
model,
)
@pytest.mark.parametrize("route", (*_OPENAI_FORMAT_ROUTES, *_SYSTEM_ONE_ROUTES))
def test_every_route_answers_box_questions_for_a_flagged_deployment_and_refuses_them_for_an_unflagged_one(
gateway: Gateway, route: str
) -> None:
flagged_member: Final = _perplexity()
plain_member: Final = _perplexity()
send: Final = (
(lambda model: _decide(gateway, model, route=route))
if route in _OPENAI_FORMAT_ROUTES
else (lambda model: _decide_in_system_one_format(gateway, model, route))
)
with wire_server(flagged_member.reply) as wire, gateway.scenario() as scenario:
flagged: Final = flagged_member.deployment(scenario, wire.url, {"supports_box_questions": True})
plain: Final = plain_member.deployment(scenario, wire.url, None)
refused: Final = send(plain)
_assert_refused_box(refused)
assert wire.drain() == ()
answered: Final = send(flagged)
assert answered.status_code == 200, answered.text
expected: Final[JsonValue] = (
_expected_answers(_QUESTIONS) if route in _OPENAI_FORMAT_ROUTES else _SYSTEM_ONE_ANSWERS
)
assert answered.json()["answers"] == expected, answered.text
(call,) = wire.drain()
assert call.target == flagged_member.path
assert _JSON_OBJECT.validate_json(call.body) == flagged_member.upstream_body(_QUESTIONS)
def test_repeated_identical_box_requests_each_reach_the_upstream_and_each_log_once(gateway: Gateway) -> None:
member: Final = _perplexity()
with wire_server(member.reply) as wire, gateway.scenario() as scenario:
model: Final = member.deployment(scenario, wire.url, {"supports_box_questions": True})
responses: Final = tuple(_decide(gateway, model, (_BOX_QUESTION,)) 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 [_JSON_OBJECT.validate_json(call.body) for call in wire.drain()] == [
member.upstream_body((_BOX_QUESTION,))
] * 2
assert [_spend_row(call_id)["status"] for call_id in call_ids] == ["success", "success"]
async def test_the_sdk_honors_a_model_info_flag_passed_by_its_caller_on_sync_and_async_calls() -> None:
member: Final = _perplexity()
with wire_server(member.reply) as wire:
common: Final[dict[str, object]] = {
"model": member.model,
"input": _INPUT,
"questions": [dict(question) for question in _QUESTIONS],
"api_base": wire.url,
"api_key": _API_KEY,
}
synchronous: Final = litellm.decisions(**common, model_info={"supports_box_questions": True})
asynchronous: Final = await litellm.adecisions(**common, model_info={"supports_box_questions": True})
for response in (synchronous, asynchronous):
assert response.model_dump(mode="json")["answers"] == _expected_answers(_QUESTIONS)
with pytest.raises(litellm.UnsupportedParamsError, match=r"\['box questions'\]"):
await litellm.adecisions(**common)
with pytest.raises(litellm.UnsupportedParamsError, match=r"\['box questions'\]"):
litellm.decisions(**common, model_info={"supports_box_questions": "true"})
assert [_JSON_OBJECT.validate_json(call.body) for call in wire.drain()] == [
member.upstream_body(_QUESTIONS)
] * 2
async def test_a_router_deployment_flagged_false_refuses_boxes_even_when_its_backend_row_says_true() -> None:
member: Final = _perplexity()
with wire_server(member.reply) as wire:
params: Final[dict[str, object]] = {"model": member.model, "api_base": wire.url, "api_key": _API_KEY}
router: Final = litellm.Router(
model_list=[
{"model_name": "box-off", "litellm_params": params, "model_info": {"supports_box_questions": False}},
{
"model_name": "box-on",
"litellm_params": params,
"model_info": {"supports_box_questions": True, "mode": "evaluation"},
},
{"model_name": "box-unset", "litellm_params": params},
]
)
common: Final[dict[str, object]] = {"input": _INPUT, "questions": [dict(question) for question in _QUESTIONS]}
with pytest.raises(litellm.UnsupportedParamsError, match=r"\['box questions'\]"):
await router.adecisions(model="box-off", **common)
assert wire.drain() == ()
answered: Final = [await router.adecisions(model=model, **common) for model in ("box-on", "box-unset")]
assert [response.model_dump(mode="json")["answers"] for response in answered] == [
_expected_answers(_QUESTIONS)
] * 2
assert [_JSON_OBJECT.validate_json(call.body) for call in wire.drain()] == [
member.upstream_body(_QUESTIONS)
] * 2
@pytest.mark.parametrize(
"value", ("true", 1, [True], "", "x" * 5000, None), ids=("string", "int", "list", "empty", "5kb", "null")
)
def test_a_non_boolean_deployment_flag_counts_as_no_flag(gateway: Gateway, value: JsonValue) -> None:
member: Final = _perplexity()
with wire_server(member.reply) as wire, gateway.scenario() as scenario:
model: Final = member.deployment(scenario, wire.url, {"supports_box_questions": value})
_assert_refused_box(_decide(gateway, model))
assert wire.drain() == ()
predicate_only: Final = _decide(gateway, model, (_PREDICATE,))
assert predicate_only.status_code == 200, predicate_only.text
assert predicate_only.json()["answers"] == [_PREDICATE_ANSWER]
(call,) = wire.drain()
assert _JSON_OBJECT.validate_json(call.body) == member.upstream_body((_PREDICATE,))
_BODY_MODEL_INFO: Final[tuple[tuple[str, JsonValue], ...]] = (
("flag", {"supports_box_questions": True}),
("int", 5),
("list", [{"supports_box_questions": True}]),
("empty", ""),
("5kb", "x" * 5000),
)
@pytest.mark.parametrize("route", ("/v1/decisions", "/v1/systemone"))
@pytest.mark.parametrize(
"value", [value for _, value in _BODY_MODEL_INFO], ids=[label for label, _ in _BODY_MODEL_INFO]
)
def test_a_body_model_info_never_turns_box_questions_on_and_never_reaches_the_provider(
gateway: Gateway, route: str, value: JsonValue
) -> None:
member: Final = _perplexity()
with wire_server(member.reply) as wire, gateway.scenario() as scenario:
model: Final = member.deployment(scenario, wire.url, None)
box: Final = (
_decide(gateway, model, route=route, model_info=value)
if route == "/v1/decisions"
else _decide_in_system_one_format(gateway, model, route, model_info=value)
)
_assert_refused_box(box)
assert wire.drain() == ()
predicate_only: Final = _decide(gateway, model, (_PREDICATE,), model_info=value)
assert predicate_only.status_code == 200, predicate_only.text
(call,) = wire.drain()
assert _JSON_OBJECT.validate_json(call.body) == member.upstream_body((_PREDICATE,))
def test_a_duplicated_or_metadata_model_info_and_a_forged_id_change_nothing_behind_the_router(
gateway: Gateway,
) -> None:
member: Final = _perplexity()
forged: Final = f"forged-{uuid.uuid4().hex}"
with wire_server(member.reply) as wire, gateway.scenario() as scenario:
model: Final = member.deployment(scenario, wire.url, None)
first: Final = json.dumps(
{
"model": model,
"input": _INPUT,
"questions": list(_QUESTIONS),
"num_retries": 0,
"model_info": {"supports_box_questions": False},
}
)
duplicated: Final = (
f'{first[:-1]}, "model_info": {json.dumps({"supports_box_questions": True, "id": forged})}}}'
)
raw: Final = gateway.client.post(
"/v1/decisions",
content=duplicated.encode(),
headers={"Authorization": f"Bearer {gateway.key}", "Content-Type": "application/json"},
)
_assert_refused_box(raw)
_assert_refused_box(_decide(gateway, model, metadata={"model_info": {"supports_box_questions": True}}))
assert wire.drain() == ()
answered: Final = _decide(gateway, model, (_PREDICATE,), model_info={"id": forged})
assert answered.status_code == 200, answered.text
assert answered.headers.get("x-litellm-model-id", "") not in ("", forged), answered.headers
assert _spend_row(answered.headers["x-litellm-call-id"])["model_id"] == answered.headers["x-litellm-model-id"]
(call,) = wire.drain()
assert _JSON_OBJECT.validate_json(call.body) == member.upstream_body((_PREDICATE,))
@pytest.mark.parametrize("status", (401, 404, 413))
def test_a_flagged_box_request_brings_the_provider_error_back_after_one_attempt(gateway: Gateway, status: int) -> None:
member: Final = _perplexity()
marker: Final = f"provider says {status} {uuid.uuid4().hex}"
def failing(_request: Request) -> Reply:
return Reply(status=status, body=json.dumps({"error": {"message": marker}}).encode())
with wire_server(failing) as wire, gateway.scenario() as scenario:
model: Final = member.deployment(scenario, wire.url, {"supports_box_questions": True})
response: Final = _decide(gateway, model)
assert response.status_code == status, response.text
assert marker in response.text, response.text
(call,) = wire.drain()
assert _JSON_OBJECT.validate_json(call.body) == member.upstream_body(_QUESTIONS)
def test_a_box_question_answered_with_the_wrong_answer_type_reaches_the_caller_as_a_refusal(gateway: Gateway) -> None:
member: Final = _perplexity()
def mismatched(_request: Request) -> Reply:
answers: Final = {"cracked": {"type": "noul", "noul": 0.9}, "crack": {"type": "noul", "noul": 0.4}}
return Reply(body=json.dumps({"model": member.body_model, "answers": answers, "usage": _USAGE}).encode())
with wire_server(mismatched) as wire, gateway.scenario() as scenario:
model: Final = member.deployment(scenario, wire.url, {"supports_box_questions": True})
response: Final = _decide(gateway, model)
assert response.status_code == 200, response.text
assert response.json()["answers"] == [_PREDICATE_ANSWER, _BOX_REFUSAL], response.text
assert len(wire.drain()) == 1
@pytest.mark.parametrize(
("header", "answers"), ((None, False), ("false", False), ("true", True)), ids=("absent", "false", "true")
)
def test_typesafe_needs_its_images_header_for_boxes_even_on_a_flagged_deployment(
gateway: Gateway, header: str | None, answers: bool
) -> None:
template: Final = _members(_tag())["typesafe"]
member: Final = _Member(
template.provider,
template.model,
template.path,
template.body_model,
headers=MappingProxyType({} if header is None else {_IMAGES_HEADER: header}),
)
with wire_server(member.reply) as wire, gateway.scenario() as scenario:
model: Final = member.deployment(scenario, wire.url, {"supports_box_questions": True})
response: Final = _decide(gateway, model)
if not answers:
_assert_refused_box(response)
assert wire.drain() == ()
return
assert response.status_code == 200, response.text
assert response.json()["answers"] == _expected_answers(_QUESTIONS)
(call,) = wire.drain()
assert call.headers.get(_IMAGES_HEADER.lower()) == "true"
assert _JSON_OBJECT.validate_json(call.body) == member.upstream_body(_QUESTIONS)
@pytest.mark.parametrize(
("flag", "keeps_box"), ((True, True), (False, False), (None, False)), ids=("true", "false", "unset")
)
def test_drop_params_drops_box_questions_unless_the_deployment_flag_is_true(
gateway: Gateway, flag: bool | None, keeps_box: bool
) -> None:
member: Final = _perplexity()
with wire_server(member.reply) as wire, gateway.scenario() as scenario:
model_info: Final[dict[str, JsonValue] | None] = None if flag is None else {"supports_box_questions": flag}
model: Final = member.deployment(scenario, wire.url, model_info, drop_params=True)
response: Final = _decide(gateway, model)
assert response.status_code == 200, response.text
sent: Final = _QUESTIONS if keeps_box else (_PREDICATE,)
answers: Final = _expected_answers(_QUESTIONS) if keeps_box else [_PREDICATE_ANSWER, _BOX_REFUSAL]
assert response.json()["answers"] == answers, response.text
(call,) = wire.drain()
assert _JSON_OBJECT.validate_json(call.body) == member.upstream_body(sent)
if keeps_box:
return
nothing_left: Final = _decide(gateway, model, (_BOX_QUESTION,))
assert nothing_left.status_code == 400, nothing_left.text
assert "leaves no questions to answer" in nothing_left.text, nothing_left.text
assert wire.drain() == ()
@pytest.mark.timeout(2 * graceful_stop_seconds() + 120)
def test_a_proxy_without_a_router_ignores_a_body_model_info_for_box_questions_and_model_ids(
gateway: Gateway, tmp_path: Path
) -> None:
member: Final = _perplexity()
forged: Final = f"forged-{uuid.uuid4().hex}"
body_model_info: Final[dict[str, JsonValue]] = {"supports_box_questions": True, "id": forged}
with wire_server(member.reply) as wire:
with configless_proxy_process(
gateway, tmp_path, {"PERPLEXITYAI_API_KEY": _API_KEY}, model=member.model, api_base=wire.url
) as owned:
box: Final = _decide(owned.gateway, member.model, model_info=body_model_info)
predicate_only: Final = _decide(owned.gateway, member.model, (_PREDICATE,), model_info=body_model_info)
system_one: Final = owned.gateway.request(
"POST",
"/v1/systemone",
{
"model": member.model,
"state": _INPUT,
"questions": {"cracked": _systemone_question(_PREDICATE)},
"model_info": body_model_info,
},
)
_assert_refused_box(box)
for answered in (predicate_only, system_one):
assert answered.status_code == 200, answered.text
assert answered.headers.get("x-litellm-model-id", "") != forged, answered.headers
assert _spend_row(answered.headers["x-litellm-call-id"])["model_id"] != forged
assert [_JSON_OBJECT.validate_json(call.body) for call in wire.drain()] == [
member.upstream_body((_PREDICATE,))
] * 2
def _new_deployment(gateway: Gateway, scenario: Scenario, member: _Member, wire: Wire) -> tuple[str, str]:
name: Final = f"integration-{uuid.uuid4().hex}"
created: Final = gateway.post(
"/model/new",
{
"model_name": name,
"litellm_params": {"model": member.model, "api_base": wire.url, "api_key": _API_KEY},
"model_info": {"supports_box_questions": True},
},
)
identity: Final = string_value(object_value(created["model_info"])["id"])
scenario.cleanups.callback(scenario.delete_model, identity)
return name, identity
def _box_outcome(gateway: Gateway, model: str, outcomes: SimpleQueue[int]) -> int:
response: Final = _decide(gateway, model, (_BOX_QUESTION,))
outcomes.put(response.status_code)
assert response.status_code in (200, 400), response.text
if response.status_code == 400:
assert _REFUSED_BOX in response.text, response.text
return 400
assert response.json()["answers"] == [_BOX_ANSWER], response.text
return 200
@pytest.mark.timeout(240)
def test_turning_the_flag_off_under_traffic_converges_without_sending_boxes_to_a_refusing_deployment(
gateway: Gateway,
) -> None:
member: Final = _perplexity()
outcomes: Final[SimpleQueue[int]] = SimpleQueue()
with wire_server(member.reply) as wire, gateway.scenario() as scenario:
model, identity = _new_deployment(gateway, scenario, member, wire)
before: Final = tuple(_box_outcome(gateway, model, outcomes) for _ in range(6))
assert before == (200,) * 6, before
patched: Final = gateway.request(
"PATCH", f"/model/{identity}/update", {"model_info": {"id": identity, "supports_box_questions": False}}
)
assert patched.status_code == 200, patched.text
during: Final = eventually(
lambda: tuple(_box_outcome(gateway, model, outcomes) for _ in range(8)),
lambda outcomes: outcomes == (400,) * 8,
seconds=3 * PROXY_CONFIG_RELOAD_INTERVAL_SECONDS,
)
assert during == (400,) * 8
received: Final = wire.drain()
statuses: Final = tuple(outcomes.get_nowait() for _ in range(outcomes.qsize()))
assert all(_JSON_OBJECT.validate_json(call.body) == member.upstream_body((_BOX_QUESTION,)) for call in received)
assert len(received) == statuses.count(200), statuses

View file

@ -2,6 +2,7 @@ from __future__ import annotations
import asyncio
import base64
import copy
import json
import re
import threading
@ -33,6 +34,7 @@ from litellm.types.decisions import (
OpenAIDecisionResponse,
ScoreAnswer,
)
from litellm.types.router import ModelInfo
from litellm.types.utils import LlmProviders
from litellm.utils import ProviderConfigManager
@ -3188,6 +3190,147 @@ async def test_box_questions_are_dropped_under_drop_params_and_answered_as_refus
assert response.model_dump(mode="json")["answers"] == _DROPPED_BOX_ANSWERS[api]
_CRACK_BOX_QUESTION: Final[Mapping[str, object]] = MappingProxyType(
{"type": "box", "name": "crack", "instructions": "the crack on the screen"}
)
_CRACK_BOX_ANSWER: Final[Mapping[str, object]] = MappingProxyType(
{"type": "box", "name": "crack", "box": {"top": 0.1, "bottom": 0.4, "left": 0.2, "right": 0.6}, "confidence": 0.7}
)
@pytest.mark.asyncio
async def test_a_deployment_that_advertises_box_questions_gets_box_answers_from_an_openai_format_provider(
respx_mock: respx.MockRouter,
) -> None:
route: Final = respx_mock.post("https://api.openai.com/v1/decisions").respond(
json={**_OPENAI_RESPONSE, "answers": [dict(_CRACK_BOX_ANSWER)]}
)
response: Final = await litellm.adecisions(
model="openai/gpt-6-luna",
input="review",
questions=[_CRACK_BOX_QUESTION],
api_key="deployment-key",
model_info={"supports_box_questions": True},
)
assert json.loads(route.calls.last.request.content)["questions"] == [dict(_CRACK_BOX_QUESTION)]
assert [answer.model_dump(mode="json") for answer in response.answers] == [dict(_CRACK_BOX_ANSWER)]
@pytest.mark.asyncio
async def test_a_deployment_that_turns_box_questions_off_is_refused_even_when_the_cost_map_allows_them(
respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(litellm, "drop_params", False)
route: Final = respx_mock.post("https://api.typesafe.ai/v1/systemone").respond(json=_RESPONSE)
with pytest.raises(litellm.UnsupportedParamsError, match=r"\['box questions'\]"):
await litellm.adecisions(
model="typesafe/jev-1.14.0",
input="review",
questions=[_CRACK_BOX_QUESTION],
api_key="deployment-key",
extra_headers={"TypeSafe-Danger-Zone-Images": "true"},
model_info={"supports_box_questions": False},
)
assert not route.called
@pytest.mark.asyncio
async def test_a_typesafe_deployment_that_advertises_box_questions_still_needs_the_images_header(
respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(litellm, "drop_params", False)
route: Final = respx_mock.post("https://api.typesafe.ai/v1/systemone").respond(json=_RESPONSE)
with pytest.raises(litellm.UnsupportedParamsError, match=r"\['box questions'\]"):
await litellm.adecisions(
model="typesafe/jev-1.14.0",
input="review",
questions=[_CRACK_BOX_QUESTION],
api_key="deployment-key",
model_info={"supports_box_questions": True},
)
assert not route.called
@pytest.mark.asyncio
async def test_a_deployment_flag_that_is_not_a_boolean_leaves_the_cost_map_in_charge(
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)
with pytest.raises(litellm.UnsupportedParamsError, match=r"\['box questions'\]"):
await litellm.adecisions(
model="openai/gpt-6-luna",
input="review",
questions=[_CRACK_BOX_QUESTION],
api_key="deployment-key",
model_info={"supports_box_questions": "true"},
)
assert not route.called
@pytest.mark.asyncio
async def test_an_unflagged_deployment_keeps_box_questions_a_same_model_deployment_advertises(
respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(litellm, "drop_params", False)
backend: Final = "perplexity/pplx-decider-v1-27b"
monkeypatch.setitem(litellm.model_cost, backend, copy.deepcopy(litellm.model_cost[backend]))
for deployment_id in ("box-flagged", "box-sibling"):
monkeypatch.setitem(litellm.model_cost, deployment_id, {})
route: Final = respx_mock.post(_PERPLEXITY_DECISIONS_URL).respond(
json={
"model": "pplx-decider-v1-27b",
"answers": {"crack": {k: v for k, v in _CRACK_BOX_ANSWER.items() if k != "name"}},
"usage": {"input_tokens": _INPUT_TOKENS, "output_tokens": _OUTPUT_TOKENS},
}
)
router: Final = litellm.Router(
model_list=[
{
"model_name": "flagged",
"litellm_params": {"model": backend, "api_key": "k"},
"model_info": {"id": "box-flagged", "supports_box_questions": True},
},
{
"model_name": "sibling",
"litellm_params": {"model": backend, "api_key": "k"},
"model_info": {"id": "box-sibling"},
},
]
)
response: Final = await router.adecisions(model="sibling", input="review", questions=[_CRACK_BOX_QUESTION])
assert "crack" in json.loads(route.calls.last.request.content)["questions"]
assert [answer.model_dump(mode="json") for answer in response.answers] == [dict(_CRACK_BOX_ANSWER)]
@pytest.mark.asyncio
async def test_a_model_info_object_instead_of_a_dict_still_reaches_the_provider(
respx_mock: respx.MockRouter,
) -> None:
route: Final = respx_mock.post("https://api.openai.com/v1/decisions").respond(json=_OPENAI_RESPONSE)
response: Final = await litellm.adecisions(
model="openai/gpt-6-luna",
input="review",
questions=[{"type": "predicate", "name": "is_defect", "instructions": "Is this a defect?"}],
api_key="deployment-key",
model_info=ModelInfo(id="deployment-1"),
)
assert route.called
assert response.answers[0].model_dump(mode="json")["name"] == "is_defect"
def test_dropping_every_question_answers_400_before_http(respx_mock: respx.MockRouter) -> None:
route: Final = respx_mock.post(_PERPLEXITY_DECISIONS_URL).respond(json=_RESPONSE)

View file

@ -422,6 +422,31 @@ def test_proxy_decisions_without_model_uses_the_proxy_default_model(
assert json.loads(upstream.calls[0].request.content)["model"] == "pplx-decider-v1-27b"
def test_a_request_body_cannot_turn_on_box_questions_for_a_proxy_without_a_router(
client: TestClient,
monkeypatch: pytest.MonkeyPatch,
respx_mock: respx.MockRouter,
) -> None:
monkeypatch.setattr(litellm, "drop_params", False)
monkeypatch.setattr(litellm.proxy.proxy_server, "llm_router", None)
monkeypatch.setattr(litellm.proxy.proxy_server, "user_model", "openai/gpt-6-luna")
upstream: Final = respx_mock.post("https://fake-openai.example/v1/decisions").respond(json={})
response: Final = client.post(
"/v1/decisions",
json={
"model": "openai/gpt-6-luna",
"input": "A phone screen with a diagonal crack.",
"questions": [{"type": "box", "name": "crack", "instructions": "Where is the crack?"}],
"model_info": {"supports_box_questions": True},
},
)
assert response.status_code == 400, response.text
assert "['box questions']" in response.json()["error"]["message"]
assert not upstream.called
_OPENAI_FORMAT_REQUEST: Final[Mapping[str, object]] = {
"model": "decider",
"input": [