Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions llm/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ("_<name>") 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:
Expand Down Expand Up @@ -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:
Expand Down
39 changes: 39 additions & 0 deletions tests/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down