diff --git a/llm/models.py b/llm/models.py index 80fb6b1a2..cee28050c 100644 --- a/llm/models.py +++ b/llm/models.py @@ -1861,6 +1861,15 @@ def execute_tool_calls( for tool_call in tool_calls_list: tool: Optional[Tool] = tools_by_name.get(tool_call.name) + if tool is None: + # Fallback: some models (e.g. Qwen via Ollama) strip the toolbox + # class-name prefix when calling a tool. Try matching by just the + # function-name suffix ("_") so "execute_js" resolves to + # "QuickJS_execute_js" when that is the only candidate. + suffix = "_" + tool_call.name + candidates = [t for n, t in tools_by_name.items() if n.endswith(suffix)] + if len(candidates) == 1: + tool = candidates[0] # Tool could be None if the tool was not found in the prompt tools, # but we still call the before_call method: if before_call: @@ -2212,6 +2221,11 @@ async def execute_tool_calls( for idx, tc in enumerate(tool_calls_list): tool: Optional[Tool] = tools_by_name.get(tc.name) + if tool is None: + suffix = "_" + tc.name + candidates = [t for n, t in tools_by_name.items() if n.endswith(suffix)] + if len(candidates) == 1: + tool = candidates[0] exception: Optional[Exception] = None if tool is None or not tool.implementation: diff --git a/tests/test_tools.py b/tests/test_tools.py index 81d01dc4b..9117ebed2 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -392,6 +392,45 @@ def simple(name: str): assert 'Error: tool \\"bad_tool\\" does not exist' in output +def test_toolbox_unqualified_name_fallback(): + """Models that strip the toolbox class-name prefix still resolve the tool.""" + model = llm.get_model("echo") + + class QuickJS(llm.Toolbox): + def execute_javascript(self, code: str): + return f"result:{code}" + + instance = QuickJS() + # Model calls "execute_javascript" but the registered name is "QuickJS_execute_javascript" + chain_response = model.chain( + json.dumps({"tool_calls": [{"name": "execute_javascript", "arguments": {"code": "1+1"}}]}), + tools=[instance], + ) + output = chain_response.text() + assert "result:1+1" in output + assert "does not exist" not in output + + +def test_toolbox_unqualified_name_ambiguous(): + """When two toolboxes share a function name, the unqualified fallback is skipped.""" + model = llm.get_model("echo") + + class Box1(llm.Toolbox): + def run(self): + return "box1" + + class Box2(llm.Toolbox): + def run(self): + return "box2" + + chain_response = model.chain( + json.dumps({"tool_calls": [{"name": "run"}]}), + tools=[Box1(), Box2()], + ) + output = chain_response.text() + assert 'Error: tool \\"run\\" does not exist' in output + + def test_tool_returning_attachment(): model = llm.get_model("echo")