forked from phoenix/litellm-mirror
Litellm dev 10 14 2024 (#6221)
* fix(__init__.py): expose DualCache, RedisCache, InMemoryCache on root abstract internal file refactors from impacting users * feat(utils.py): handle invalid openai parallel tool calling response Fixes https://community.openai.com/t/model-tries-to-call-unknown-function-multi-tool-use-parallel/490653 * docs(bedrock.md): clarify all bedrock models are supported Closes https://github.com/BerriAI/litellm/issues/6168#issuecomment-2412082236
This commit is contained in:
parent
cda0a993e2
commit
39486e2003
5 changed files with 240 additions and 5 deletions
|
@ -4567,3 +4567,176 @@ def test_completion_response_ratelimit_headers(model, stream):
|
|||
assert v != "None" and v is not None
|
||||
assert "x-ratelimit-remaining-requests" in additional_headers
|
||||
assert "x-ratelimit-remaining-tokens" in additional_headers
|
||||
|
||||
|
||||
def _openai_hallucinated_tool_call_mock_response(
|
||||
*args, **kwargs
|
||||
) -> litellm.ModelResponse:
|
||||
new_response = MagicMock()
|
||||
new_response.headers = {"hello": "world"}
|
||||
|
||||
response_object = {
|
||||
"id": "chatcmpl-123",
|
||||
"object": "chat.completion",
|
||||
"created": 1677652288,
|
||||
"model": "gpt-3.5-turbo-0125",
|
||||
"system_fingerprint": "fp_44709d6fcb",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"content": None,
|
||||
"role": "assistant",
|
||||
"tool_calls": [
|
||||
{
|
||||
"function": {
|
||||
"arguments": '{"tool_uses":[{"recipient_name":"product_title","parameters":{"content":"Story Scribe"}},{"recipient_name":"one_liner","parameters":{"content":"Transform interview transcripts into actionable user stories"}}]}',
|
||||
"name": "multi_tool_use.parallel",
|
||||
},
|
||||
"id": "call_IzGXwVa5OfBd9XcCJOkt2q0s",
|
||||
"type": "function",
|
||||
}
|
||||
],
|
||||
},
|
||||
"logprobs": None,
|
||||
"finish_reason": "stop",
|
||||
}
|
||||
],
|
||||
"usage": {"prompt_tokens": 9, "completion_tokens": 12, "total_tokens": 21},
|
||||
}
|
||||
from openai import OpenAI
|
||||
from openai.types.chat.chat_completion import ChatCompletion
|
||||
|
||||
pydantic_obj = ChatCompletion(**response_object) # type: ignore
|
||||
pydantic_obj.choices[0].message.role = None # type: ignore
|
||||
new_response.parse.return_value = pydantic_obj
|
||||
return new_response
|
||||
|
||||
|
||||
def test_openai_hallucinated_tool_call():
|
||||
"""
|
||||
Patch for this issue: https://community.openai.com/t/model-tries-to-call-unknown-function-multi-tool-use-parallel/490653
|
||||
|
||||
Handle openai invalid tool calling response.
|
||||
|
||||
OpenAI assistant will sometimes return an invalid tool calling response, which needs to be parsed
|
||||
|
||||
- "arguments": "{\"tool_uses\":[{\"recipient_name\":\"product_title\",\"parameters\":{\"content\":\"Story Scribe\"}},{\"recipient_name\":\"one_liner\",\"parameters\":{\"content\":\"Transform interview transcripts into actionable user stories\"}}]}",
|
||||
|
||||
To extract actual tool calls:
|
||||
|
||||
1. Parse arguments JSON object
|
||||
2. Iterate over tool_uses array to call functions:
|
||||
- get function name from recipient_name value
|
||||
- parameters will be JSON object for function arguments
|
||||
"""
|
||||
import openai
|
||||
|
||||
openai_client = openai.OpenAI()
|
||||
with patch.object(
|
||||
openai_client.chat.completions,
|
||||
"create",
|
||||
side_effect=_openai_hallucinated_tool_call_mock_response,
|
||||
) as mock_response:
|
||||
response = litellm.completion(
|
||||
model="gpt-3.5-turbo",
|
||||
messages=[{"role": "user", "content": "Hey! how's it going?"}],
|
||||
client=openai_client,
|
||||
)
|
||||
print(f"response: {response}")
|
||||
|
||||
response_dict = response.model_dump()
|
||||
|
||||
tool_calls = response_dict["choices"][0]["message"]["tool_calls"]
|
||||
|
||||
print(f"tool_calls: {tool_calls}")
|
||||
|
||||
for idx, tc in enumerate(tool_calls):
|
||||
if idx == 0:
|
||||
print(f"tc in test_openai_hallucinated_tool_call: {tc}")
|
||||
assert tc == {
|
||||
"function": {
|
||||
"arguments": '{"content": "Story Scribe"}',
|
||||
"name": "product_title",
|
||||
},
|
||||
"id": "call_IzGXwVa5OfBd9XcCJOkt2q0s_0",
|
||||
"type": "function",
|
||||
}
|
||||
elif idx == 1:
|
||||
assert tc == {
|
||||
"function": {
|
||||
"arguments": '{"content": "Transform interview transcripts into actionable user stories"}',
|
||||
"name": "one_liner",
|
||||
},
|
||||
"id": "call_IzGXwVa5OfBd9XcCJOkt2q0s_1",
|
||||
"type": "function",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"function_name, expect_modification",
|
||||
[
|
||||
("multi_tool_use.parallel", True),
|
||||
("my-fake-function", False),
|
||||
],
|
||||
)
|
||||
def test_openai_hallucinated_tool_call_util(function_name, expect_modification):
|
||||
"""
|
||||
Patch for this issue: https://community.openai.com/t/model-tries-to-call-unknown-function-multi-tool-use-parallel/490653
|
||||
|
||||
Handle openai invalid tool calling response.
|
||||
|
||||
OpenAI assistant will sometimes return an invalid tool calling response, which needs to be parsed
|
||||
|
||||
- "arguments": "{\"tool_uses\":[{\"recipient_name\":\"product_title\",\"parameters\":{\"content\":\"Story Scribe\"}},{\"recipient_name\":\"one_liner\",\"parameters\":{\"content\":\"Transform interview transcripts into actionable user stories\"}}]}",
|
||||
|
||||
To extract actual tool calls:
|
||||
|
||||
1. Parse arguments JSON object
|
||||
2. Iterate over tool_uses array to call functions:
|
||||
- get function name from recipient_name value
|
||||
- parameters will be JSON object for function arguments
|
||||
"""
|
||||
from litellm.utils import _handle_invalid_parallel_tool_calls
|
||||
from litellm.types.utils import ChatCompletionMessageToolCall
|
||||
|
||||
response = _handle_invalid_parallel_tool_calls(
|
||||
tool_calls=[
|
||||
ChatCompletionMessageToolCall(
|
||||
**{
|
||||
"function": {
|
||||
"arguments": '{"tool_uses":[{"recipient_name":"product_title","parameters":{"content":"Story Scribe"}},{"recipient_name":"one_liner","parameters":{"content":"Transform interview transcripts into actionable user stories"}}]}',
|
||||
"name": function_name,
|
||||
},
|
||||
"id": "call_IzGXwVa5OfBd9XcCJOkt2q0s",
|
||||
"type": "function",
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
print(f"response: {response}")
|
||||
|
||||
if expect_modification:
|
||||
for idx, tc in enumerate(response):
|
||||
if idx == 0:
|
||||
assert tc.model_dump() == {
|
||||
"function": {
|
||||
"arguments": '{"content": "Story Scribe"}',
|
||||
"name": "product_title",
|
||||
},
|
||||
"id": "call_IzGXwVa5OfBd9XcCJOkt2q0s_0",
|
||||
"type": "function",
|
||||
}
|
||||
elif idx == 1:
|
||||
assert tc.model_dump() == {
|
||||
"function": {
|
||||
"arguments": '{"content": "Transform interview transcripts into actionable user stories"}',
|
||||
"name": "one_liner",
|
||||
},
|
||||
"id": "call_IzGXwVa5OfBd9XcCJOkt2q0s_1",
|
||||
"type": "function",
|
||||
}
|
||||
else:
|
||||
assert len(response) == 1
|
||||
assert response[0].function.name == function_name
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue