@@ -509,13 +509,29 @@ def process_model_response(
509509 # Regular function tool call
510510 else:
511511 if output.name not in function_map:
512- _error_tracing.attach_error_to_current_span(
513- SpanError(
514- message="Tool not found",
515- data={"tool_name": output.name},
512+ if output_schema is not None and output.name == "json_tool_call":
513+ # LiteLLM could generate non-existent tool calls for structured outputs
514+ items.append(ToolCallItem(raw_item=output, agent=agent))
515+ functions.append(
516+ ToolRunFunction(
517+ tool_call=output,
518+ # this tool does not exist in function_map, so generate ad-hoc one,
519+ # which just parses the input if it's a string, and returns the
520+ # value otherwise
521+ function_tool=_build_litellm_json_tool_call(output),
522+ )
516523 )
517- )
518- raise ModelBehaviorError(f"Tool {output.name} not found in agent {agent.name}")
524+ continue
525+ else:
526+ _error_tracing.attach_error_to_current_span(
527+ SpanError(
528+ message="Tool not found",
529+ data={"tool_name": output.name},
530+ )
531+ )
532+ error = f"Tool {output.name} not found in agent {agent.name}"
533+ raise ModelBehaviorError(error)
534+
519535 items.append(ToolCallItem(raw_item=output, agent=agent))
520536 functions.append(
521537 ToolRunFunction(
@@ -1193,3 +1209,21 @@ async def execute(
11931209 # "id": "out" + call.tool_call.id, # TODO remove this, it should be optional
11941210 },
11951211 )
1212+
1213+
1214+ def _build_litellm_json_tool_call(output: ResponseFunctionToolCall) -> FunctionTool:
1215+ async def on_invoke_tool(_ctx: ToolContext[Any], value: Any) -> Any:
1216+ if isinstance(value, str):
1217+ import json
1218+
1219+ return json.loads(value)
1220+ return value
1221+
1222+ return FunctionTool(
1223+ name=output.name,
1224+ description=output.name,
1225+ params_json_schema={},
1226+ on_invoke_tool=on_invoke_tool,
1227+ strict_json_schema=True,
1228+ is_enabled=True,
1229+ )
0 commit comments