mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-25 01:02:15 +00:00
fix(guardrails): keep the assistant turn when scoping empties the request history
A request whose turns all fall outside the guardrail's scope, such as a user-only request under scan_only_tool_results, still supplied a conversation, so the response scan now carries the reply as the sole assistant turn instead of dropping structured_messages. Response-only behavior stays when no conversation was supplied Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
43ae9aff3d
commit
d5e056491c
4 changed files with 31 additions and 2 deletions
|
|
@ -30,6 +30,7 @@ class RequestScanContext:
|
|||
|
||||
structured_messages: tuple["AllMessageValues", ...] = ()
|
||||
tools: tuple["ChatCompletionToolParam", ...] = ()
|
||||
conversation_supplied: bool = False
|
||||
|
||||
@staticmethod
|
||||
def scoped(
|
||||
|
|
@ -51,6 +52,7 @@ class RequestScanContext:
|
|||
return RequestScanContext(
|
||||
structured_messages=tuple(structured_messages[index] for index in scoped_indices),
|
||||
tools=() if scan_only_tool_results else tuple(tools),
|
||||
conversation_supplied=bool(structured_messages),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -308,7 +310,7 @@ class BaseTranslation(ABC):
|
|||
if request_data is None:
|
||||
return inputs
|
||||
context: Final = self.request_scan_context(request_data, guardrail_to_apply)
|
||||
if not context.structured_messages:
|
||||
if not context.conversation_supplied:
|
||||
return inputs
|
||||
assistant_turn: Final = response_assistant_turn(inputs.get("texts") or (), inputs.get("tool_calls") or ())
|
||||
contextual_inputs: Final[GenericGuardrailAPIInputs] = {
|
||||
|
|
|
|||
|
|
@ -454,8 +454,9 @@ class OpenAIResponsesHandler(BaseTranslation):
|
|||
|
||||
def request_scan_context(self, data: dict, guardrail_to_apply: "CustomGuardrail") -> RequestScanContext:
|
||||
raw_tools: Final = data.get("tools")
|
||||
structured_messages: Final = tuple(self.get_structured_messages(data) or ())
|
||||
return RequestScanContext(
|
||||
structured_messages=tuple(self.get_structured_messages(data) or ()),
|
||||
structured_messages=structured_messages,
|
||||
tools=tuple(
|
||||
cast(ChatCompletionToolParam, tool) # cast-ok: mcp tools ride along in the guardrail's tool list
|
||||
for form in LiteLLMCompletionResponsesConfig.responses_tools_to_chat_forms(
|
||||
|
|
@ -463,6 +464,7 @@ class OpenAIResponsesHandler(BaseTranslation):
|
|||
)
|
||||
for tool in form.chat_tools
|
||||
),
|
||||
conversation_supplied=bool(structured_messages),
|
||||
)
|
||||
|
||||
async def process_input_messages(
|
||||
|
|
|
|||
|
|
@ -2360,6 +2360,19 @@ class TestResponseScanCarriesRequestConversation:
|
|||
assert [m["role"] for m in inputs["structured_messages"]] == ["tool", "assistant"]
|
||||
assert "tools" not in inputs
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_scan_only_tool_results_without_tool_turns_still_carries_the_reply(self):
|
||||
handler = OpenAIChatCompletionsHandler()
|
||||
guardrail = InputsRecordingGuardrail()
|
||||
guardrail.scan_only_tool_results = True
|
||||
request = {**self._request(), "messages": [{"role": "user", "content": "Delete everything"}]}
|
||||
|
||||
await handler.process_output_response(self._tool_call_response(), guardrail, request_data=request)
|
||||
|
||||
[(_, inputs)] = guardrail.seen
|
||||
assert [m["role"] for m in inputs["structured_messages"]] == ["assistant"]
|
||||
assert inputs["structured_messages"][0]["tool_calls"][0]["function"]["name"] == "run_shell"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_response_scan_without_request_data_stays_response_only(self):
|
||||
guardrail = InputsRecordingGuardrail()
|
||||
|
|
|
|||
|
|
@ -3397,3 +3397,15 @@ class TestResponsesResponseScanCarriesRequestConversation:
|
|||
"assistant",
|
||||
]
|
||||
assert inputs["structured_messages"][-1] == {"role": "assistant", "content": "Paris is the capital"}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_response_scan_without_request_input_stays_response_only(self):
|
||||
handler = OpenAIResponsesHandler()
|
||||
guardrail = TypedInputsRecordingGuardrail()
|
||||
request = {k: v for k, v in self._request().items() if k not in ("input", "instructions")}
|
||||
|
||||
await handler.process_output_response(self._tool_call_response(), guardrail, request_data=request)
|
||||
|
||||
[(_, inputs)] = guardrail.seen
|
||||
assert "structured_messages" not in inputs
|
||||
assert "tools" not in inputs
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue