Skip to content

Commit 187d6db

Browse files
committed
fix: Use convert_tooldef_to_openai_tool for function tool serialization
The Responses API function tool conversion used model_dump() to build the ChatCompletionToolParam function dict, which leaked the "type" field (and would leak any future fields like defer_loading) into the FunctionDefinition. OpenAI silently ignores extra fields, but stricter endpoints (e.g. Gemini) reject them. Instead of a fragile denylist (exclude={"type"}), reuse convert_tooldef_to_openai_tool which explicitly picks only the fields that belong in a FunctionDefinition. Added strict parameter support to convert_tooldef_to_openai_tool so function tools can pass it through. Signed-off-by: Dan Prince <dprince@redhat.com>
1 parent b02461b commit 187d6db

3 files changed

Lines changed: 52 additions & 2 deletions

File tree

src/llama_stack/providers/inline/agents/meta_reference/responses/streaming.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1398,8 +1398,13 @@ def make_openai_tool(tool_name: str, tool: ToolDef) -> ChatCompletionToolParam:
13981398
for input_tool in tools:
13991399
if input_tool.type == "function":
14001400
self.ctx.chat_tools.append(
1401-
ChatCompletionToolParam(type="function", function=input_tool.model_dump(exclude_none=True))
1402-
) # type: ignore[typeddict-item,arg-type] # Dict compatible with FunctionDefinition
1401+
convert_tooldef_to_openai_tool(
1402+
tool_name=input_tool.name,
1403+
description=input_tool.description,
1404+
input_schema=input_tool.parameters,
1405+
strict=input_tool.strict,
1406+
)
1407+
) # type: ignore[arg-type] # Returns dict but ChatCompletionToolParam expects TypedDict
14031408
elif input_tool.type in WebSearchToolTypes:
14041409
tool_name = "web_search"
14051410
# Need to access tool_groups_api from tool_executor

src/llama_stack/providers/utils/inference/openai_compat.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def convert_tooldef_to_openai_tool(
1818
tool_name: str,
1919
description: str | None = None,
2020
input_schema: dict[str, Any] | None = None,
21+
strict: bool | None = None,
2122
) -> dict[str, Any]:
2223
"""
2324
Convert tool parameters to an OpenAI API-compatible dictionary.
@@ -26,6 +27,7 @@ def convert_tooldef_to_openai_tool(
2627
tool_name: Tool name as string
2728
description: Optional tool description
2829
input_schema: Optional JSON Schema for tool parameters
30+
strict: Optional flag for strict parameter validation
2931
3032
Returns:
3133
OpenAI-compatible tool dictionary:
@@ -35,6 +37,7 @@ def convert_tooldef_to_openai_tool(
3537
"name": tool_name,
3638
"description": description,
3739
"parameters": {<JSON Schema>},
40+
"strict": strict,
3841
},
3942
}
4043
@@ -54,6 +57,9 @@ def convert_tooldef_to_openai_tool(
5457
if input_schema:
5558
function["parameters"] = input_schema
5659

60+
if strict is not None:
61+
function["strict"] = strict
62+
5763
return out
5864

5965

tests/unit/providers/agents/meta_reference/test_openai_responses.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2540,6 +2540,45 @@ async def test_function_tool_strict_false_included(openai_responses_impl, mock_i
25402540
assert tool_function["strict"] is False, "strict field should be False"
25412541

25422542

2543+
async def test_function_tool_type_field_excluded_from_function_dict(openai_responses_impl, mock_inference_api):
2544+
"""Test that the 'type' field from the input tool is excluded from the function dict.
2545+
2546+
The function dict inside ChatCompletionToolParam should only contain function-specific
2547+
fields (name, description, parameters, strict) and not the top-level 'type' field,
2548+
which belongs on the outer tool param.
2549+
"""
2550+
input_text = "What is the weather?"
2551+
model = "meta-llama/Llama-3.1-8B-Instruct"
2552+
2553+
mock_inference_api.openai_chat_completion.return_value = fake_stream()
2554+
2555+
await openai_responses_impl.create_openai_response(
2556+
input=input_text,
2557+
model=model,
2558+
stream=False,
2559+
tools=[
2560+
OpenAIResponseInputToolFunction(
2561+
type="function",
2562+
name="get_weather",
2563+
description="Get weather information",
2564+
parameters={"type": "object", "properties": {"location": {"type": "string"}}, "required": ["location"]},
2565+
)
2566+
],
2567+
)
2568+
2569+
assert mock_inference_api.openai_chat_completion.call_count == 1
2570+
params = mock_inference_api.openai_chat_completion.call_args[0][0]
2571+
2572+
assert len(params.tools) == 1
2573+
# The outer tool param should have type="function"
2574+
assert params.tools[0]["type"] == "function"
2575+
# The inner function dict should NOT contain 'type'
2576+
tool_function = params.tools[0]["function"]
2577+
assert "type" not in tool_function, "The input tool 'type' field should be excluded from the function dict"
2578+
assert tool_function["name"] == "get_weather"
2579+
assert tool_function["description"] == "Get weather information"
2580+
2581+
25432582
async def test_create_openai_response_with_truncation_disabled_streaming(
25442583
openai_responses_impl, mock_inference_api, mock_responses_store
25452584
):

0 commit comments

Comments
 (0)