Compare commits

...

1 Commits

Author SHA1 Message Date
openhands
530153f54b Fix issue #7968: [Bug]: o4-mini is incompatible and throws an error. 2025-04-22 20:38:54 +00:00
3 changed files with 81 additions and 6 deletions

View File

@@ -265,7 +265,7 @@ def convert_tool_call_to_string(tool_call: dict) -> str:
return ret
def convert_tools_to_description(tools: list[dict]) -> str:
def convert_tools_to_description(tools: list[dict], max_desc_length: int = None) -> str:
ret = ''
for i, tool in enumerate(tools):
assert tool['type'] == 'function'
@@ -273,7 +273,12 @@ def convert_tools_to_description(tools: list[dict]) -> str:
if i > 0:
ret += '\n'
ret += f"---- BEGIN FUNCTION #{i+1}: {fn['name']} ----\n"
ret += f"Description: {fn['description']}\n"
# Truncate description if needed
desc = fn['description']
if max_desc_length and len(desc) > max_desc_length:
desc = desc[:max_desc_length-3] + "..."
ret += f"Description: {desc}\n"
if 'parameters' in fn:
ret += 'Parameters:\n'
@@ -286,13 +291,21 @@ def convert_tools_to_description(tools: list[dict]) -> str:
param_status = 'required' if is_required else 'optional'
param_type = param_info.get('type', 'string')
# Get parameter description
# Get parameter description and truncate if needed
desc = param_info.get('description', 'No description provided')
if max_desc_length and len(desc) > max_desc_length:
desc = desc[:max_desc_length-3] + "..."
# Handle enum values if present
if 'enum' in param_info:
enum_values = ', '.join(f'`{v}`' for v in param_info['enum'])
desc += f'\nAllowed values: [{enum_values}]'
enum_desc = f'\nAllowed values: [{enum_values}]'
if max_desc_length and len(desc + enum_desc) > max_desc_length:
# Only add enum values if there's room
if len(desc) + 20 <= max_desc_length: # Leave room for "[enum1, enum2,...]"
desc = desc[:max_desc_length-20] + f"\nAllowed values: [...]"
else:
desc += enum_desc
ret += (
f' ({j+1}) {param_name} ({param_type}, {param_status}): {desc}\n'
@@ -308,6 +321,7 @@ def convert_fncall_messages_to_non_fncall_messages(
messages: list[dict],
tools: list[ChatCompletionToolParam],
add_in_context_learning_example: bool = True,
max_desc_length: int = None,
) -> list[dict]:
"""Convert function calling messages to non-function calling messages."""
messages = copy.deepcopy(messages)
@@ -562,10 +576,11 @@ def _fix_stopword(content: str) -> str:
def convert_non_fncall_messages_to_fncall_messages(
messages: list[dict],
tools: list[ChatCompletionToolParam],
max_desc_length: int = None,
) -> list[dict]:
"""Convert non-function calling messages back to function calling messages."""
messages = copy.deepcopy(messages)
formatted_tools = convert_tools_to_description(tools)
formatted_tools = convert_tools_to_description(tools, max_desc_length=max_desc_length)
system_prompt_suffix = SYSTEM_PROMPT_SUFFIX_TEMPLATE.format(
description=formatted_tools
)

View File

@@ -225,12 +225,15 @@ class LLM(RetryMixin, DebugMixin):
mock_fncall_tools = None
# if the agent or caller has defined tools, and we mock via prompting, convert the messages
if mock_function_calling and 'tools' in kwargs:
# Handle description length limit for o4-mini
max_desc_length = 1024 if 'o4-mini' in self.config.model else None
messages = convert_fncall_messages_to_non_fncall_messages(
messages,
kwargs['tools'],
add_in_context_learning_example=bool(
'openhands-lm' not in self.config.model
),
max_desc_length=max_desc_length,
)
kwargs['messages'] = messages
@@ -290,9 +293,13 @@ class LLM(RetryMixin, DebugMixin):
)
non_fncall_response_message = resp.choices[0].message
# Handle description length limit for o4-mini
max_desc_length = 1024 if 'o4-mini' in self.config.model else None
fn_call_messages_with_response = (
convert_non_fncall_messages_to_fncall_messages(
messages + [non_fncall_response_message], mock_fncall_tools
messages + [non_fncall_response_message],
mock_fncall_tools,
max_desc_length=max_desc_length
)
)
fn_call_response_message = fn_call_messages_with_response[-1]

View File

@@ -0,0 +1,53 @@
"""Test that LLM handles description length limits correctly."""
import pytest
from litellm import ChatCompletionToolParam, ChatCompletionToolParamFunctionChunk
from openhands.core.config import LLMConfig
from openhands.llm.fn_call_converter import convert_tools_to_description
from openhands.llm.llm import LLM
def test_description_length_limit():
"""Test that description length is limited for o4-mini model."""
# Create a tool with a long description
long_desc = "A" * 2000 # Description longer than 1024 chars
tool = ChatCompletionToolParam(
type='function',
function=ChatCompletionToolParamFunctionChunk(
name='test_function',
description=long_desc,
parameters={
'type': 'object',
'properties': {
'param1': {
'type': 'string',
'description': long_desc,
}
},
'required': ['param1'],
},
),
)
# Test with o4-mini model
config = LLMConfig(model='openai/o4-mini')
llm = LLM(config)
# Convert tools to description with max length limit
desc = convert_tools_to_description([tool], max_desc_length=1024)
# Verify description is truncated
assert len(desc.split('\nDescription: ')[1].split('\n')[0]) <= 1024
assert len(desc.split('param1 (string, required): ')[1].split('\n')[0]) <= 1024
# Test without length limit
config = LLMConfig(model='gpt-4')
llm = LLM(config)
# Convert tools to description without max length limit
desc = convert_tools_to_description([tool])
# Verify description is not truncated
assert len(desc.split('\nDescription: ')[1].split('\n')[0]) == 2000
assert len(desc.split('param1 (string, required): ')[1].split('\n')[0]) == 2000