mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-16 23:41:43 +00:00
fix(responses): classify streamed tool calls on the chat name and strip guardrail edits around the grammar block
The streaming bridge restored the namespace before deciding whether a tool call was a custom tool, so a namespaced function sharing a short name with a nested custom tool streamed back as a custom_tool_call. Classify on the raw chat tool name first, the way the non-streaming path already does. The guardrail merge only stripped the namespace prefix and grammar suffix from the ends of the edited description, so a guardrail appending text after the grammar block left the block in the member description and the chat conversion appended it a second time. Strip the first occurrence of each instead.
This commit is contained in:
parent
7ea19eccc7
commit
e3152c011d
4 changed files with 105 additions and 15 deletions
|
|
@ -72,7 +72,7 @@ def _function_fields(tool: Tool) -> Tool:
|
|||
def _member_description(key: str, value: object, prefix: str, suffix: str) -> object:
|
||||
if key != "description" or not isinstance(value, str):
|
||||
return value
|
||||
return value.removeprefix(prefix).removesuffix(suffix)
|
||||
return value.replace(prefix, "", 1).replace(suffix, "", 1)
|
||||
|
||||
|
||||
def _rebuilt_member(member: Tool, flattened: Tool, guardrailed: Tool, namespace_description: str) -> Tool:
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from litellm.main import stream_chunk_builder
|
|||
from litellm.responses.litellm_completion_transformation.custom_tools import (
|
||||
build_tool_call_item_kwargs,
|
||||
extract_custom_tool_names,
|
||||
is_custom_tool_call,
|
||||
serialize_tool_call_arguments,
|
||||
)
|
||||
from litellm.responses.litellm_completion_transformation.transformation import (
|
||||
|
|
@ -166,6 +167,14 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator):
|
|||
return tool_name, namespace
|
||||
return fn_name, None
|
||||
|
||||
def _tool_call_item_kwargs(self, call_id: str, fn_name: str, arguments: str, status: str) -> dict[str, str]:
|
||||
item_kwargs: Final = build_tool_call_item_kwargs(call_id, fn_name, arguments, status, self._custom_tool_names)
|
||||
if is_custom_tool_call(fn_name, self._custom_tool_names):
|
||||
return item_kwargs
|
||||
tool_name, tool_namespace = self._responses_namespace_tool_call_fields(fn_name)
|
||||
namespace_kwargs: Final = {"namespace": tool_namespace} if tool_namespace else {}
|
||||
return {**item_kwargs, "name": tool_name, **namespace_kwargs}
|
||||
|
||||
def _is_reasoning_end(self, chunk):
|
||||
delta: Final = chunk.choices[0].delta
|
||||
|
||||
|
|
@ -244,17 +253,13 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator):
|
|||
else:
|
||||
fn_name = str(getattr(fn, "name", "") or "")
|
||||
fn_args_delta = serialize_tool_call_arguments(getattr(fn, "arguments", ""))
|
||||
tool_name, tool_namespace = self._responses_namespace_tool_call_fields(fn_name)
|
||||
output_index = self._get_or_assign_tool_output_index(call_id)
|
||||
|
||||
if call_id not in self._tool_args_by_call_id:
|
||||
self._tool_args_by_call_id[call_id] = ""
|
||||
self._sequence_number += 1
|
||||
names = self._custom_tool_names
|
||||
item_kwargs = build_tool_call_item_kwargs(call_id, tool_name, "", "in_progress", names)
|
||||
item_kwargs = self._tool_call_item_kwargs(call_id, fn_name, "", "in_progress")
|
||||
self._tool_item_id_by_call_id[call_id] = item_kwargs["id"]
|
||||
if tool_namespace:
|
||||
item_kwargs["namespace"] = tool_namespace
|
||||
event = OutputItemAddedEvent(
|
||||
type=ResponsesAPIStreamEvents.OUTPUT_ITEM_ADDED,
|
||||
output_index=output_index,
|
||||
|
|
@ -315,7 +320,6 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator):
|
|||
else:
|
||||
fn_name = str(getattr(fn, "name", "") or "")
|
||||
fn_args = serialize_tool_call_arguments(getattr(fn, "arguments", ""))
|
||||
tool_name, tool_namespace = self._responses_namespace_tool_call_fields(fn_name)
|
||||
web_search_call = self._web_search_calls.get(call_id)
|
||||
if web_search_call is not None:
|
||||
if call_id not in self._queued_web_search_call_ids:
|
||||
|
|
@ -330,11 +334,8 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator):
|
|||
if is_new_tool_call:
|
||||
self._tool_args_by_call_id[call_id] = ""
|
||||
self._sequence_number += 1
|
||||
names = self._custom_tool_names
|
||||
item_kwargs = build_tool_call_item_kwargs(call_id, tool_name, "", "in_progress", names)
|
||||
item_kwargs = self._tool_call_item_kwargs(call_id, fn_name, "", "in_progress")
|
||||
self._tool_item_id_by_call_id[call_id] = item_kwargs["id"]
|
||||
if tool_namespace:
|
||||
item_kwargs["namespace"] = tool_namespace
|
||||
event = OutputItemAddedEvent(
|
||||
type=ResponsesAPIStreamEvents.OUTPUT_ITEM_ADDED,
|
||||
output_index=output_index,
|
||||
|
|
@ -376,11 +377,8 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator):
|
|||
self._pending_tool_events.append(done_event)
|
||||
|
||||
self._sequence_number += 1
|
||||
names = self._custom_tool_names
|
||||
item_kwargs = build_tool_call_item_kwargs(call_id, tool_name, final_args, "completed", names)
|
||||
item_kwargs = self._tool_call_item_kwargs(call_id, fn_name, final_args, "completed")
|
||||
item_kwargs["id"] = self._tool_item_id_by_call_id.setdefault(call_id, item_kwargs["id"])
|
||||
if tool_namespace:
|
||||
item_kwargs["namespace"] = tool_namespace
|
||||
item_done_event = OutputItemDoneEvent(
|
||||
type=ResponsesAPIStreamEvents.OUTPUT_ITEM_DONE,
|
||||
output_index=output_index,
|
||||
|
|
|
|||
|
|
@ -174,6 +174,21 @@ def test_custom_member_description_edit_lands_without_the_namespace_prefix_or_gr
|
|||
assert list(merged) == [{"type": "namespace", "name": "shell", "description": "Shell", "tools": [guarded_member]}]
|
||||
|
||||
|
||||
def test_text_appended_after_the_grammar_block_lands_on_the_member_without_the_block():
|
||||
grammar = {"type": "grammar", "syntax": "lark", "definition": "start: X"}
|
||||
custom_member = {"type": "custom", "name": "exec", "description": "Run a command", "format": grammar}
|
||||
original = [{"type": "namespace", "name": "shell", "description": "Shell", "tools": [custom_member]}]
|
||||
groups = _groups(original)
|
||||
edited = copy.deepcopy(_flat(groups))
|
||||
edited[0]["function"]["description"] = edited[0]["function"]["description"] + " [checked]"
|
||||
|
||||
merged = merge_guardrailed_tools(original, groups, edited)
|
||||
|
||||
assert merged[0]["tools"][0]["description"] == "Run a command [checked]"
|
||||
reflattened = _flat(_groups(merged))
|
||||
assert reflattened[0]["function"]["description"] == "Shell\n\nRun a command [checked]\n\nFormat:\n```lark\nstart: X\n```"
|
||||
|
||||
|
||||
def test_member_extras_edited_by_the_guardrail_land_on_that_member():
|
||||
original = [{"type": "namespace", "name": "ns", "tools": [_function("read")]}]
|
||||
groups = _groups(original)
|
||||
|
|
|
|||
|
|
@ -3877,6 +3877,83 @@ class TestEnsureOutputItemContentPartAdded:
|
|||
assert done.item.type == "custom_tool_call"
|
||||
assert done.item.input == "ls"
|
||||
|
||||
def test_streaming_namespaced_function_sharing_a_nested_custom_short_name_stays_a_function_call(self):
|
||||
from litellm.responses.litellm_completion_transformation.custom_tools import extract_custom_tool_names
|
||||
|
||||
iterator = self._make_iterator()
|
||||
iterator.responses_api_request = {
|
||||
"tools": [
|
||||
{
|
||||
"type": "namespace",
|
||||
"name": "alpha",
|
||||
"tools": [
|
||||
{
|
||||
"type": "custom",
|
||||
"name": "run",
|
||||
"format": {"type": "grammar", "syntax": "lark", "definition": "start: /.+/"},
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "namespace",
|
||||
"name": "beta",
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"name": "run",
|
||||
"parameters": {"type": "object", "properties": {"job_id": {"type": "string"}}},
|
||||
}
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
iterator._custom_tool_names = extract_custom_tool_names(iterator.responses_api_request.get("tools"))
|
||||
iterator._namespace_tool_names = LiteLLMCompletionResponsesConfig.namespace_tool_name_map(
|
||||
iterator.responses_api_request.get("tools")
|
||||
)
|
||||
function_call = {"id": "call_fn", "function": {"name": "beta__run", "arguments": '{"job_id":"42"}'}}
|
||||
custom_call = {"id": "call_custom", "function": {"name": "run", "arguments": '{"content":"echo hi"}'}}
|
||||
|
||||
iterator._queue_tool_call_delta_events([{"index": 0, **function_call}, {"index": 1, **custom_call}])
|
||||
iterator._queue_final_tool_call_done_events(
|
||||
ModelResponse(
|
||||
id="chatcmpl-run",
|
||||
created=1,
|
||||
model="us.openai.gpt-5.6",
|
||||
object="chat.completion",
|
||||
choices=[
|
||||
Choices(
|
||||
finish_reason="tool_calls",
|
||||
index=0,
|
||||
message=Message(
|
||||
content=None,
|
||||
role="assistant",
|
||||
tool_calls=[
|
||||
ChatCompletionMessageToolCall(
|
||||
id=call["id"], type="function", function=Function(**call["function"])
|
||||
)
|
||||
for call in (function_call, custom_call)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
items = [
|
||||
event.item
|
||||
for event in iterator._pending_tool_events
|
||||
if event.type in ("response.output_item.added", "response.output_item.done")
|
||||
]
|
||||
function_items = [item for item in items if item.call_id == "call_fn"]
|
||||
custom_items = [item for item in items if item.call_id == "call_custom"]
|
||||
assert len(function_items) == 2 and len(custom_items) == 2
|
||||
assert all((item.type, item.name, item.namespace) == ("function_call", "run", "beta") for item in function_items)
|
||||
assert function_items[-1].arguments == '{"job_id":"42"}'
|
||||
assert all(item.type == "custom_tool_call" and item.name == "run" for item in custom_items)
|
||||
assert all(getattr(item, "namespace", None) is None for item in custom_items)
|
||||
assert custom_items[-1].input == "echo hi"
|
||||
|
||||
def test_streaming_unqualified_namespace_tool_calls_restore_namespace(self):
|
||||
"""A unique nested tool name without the namespace still maps back."""
|
||||
iterator = self._make_iterator()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue