mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-12 09:52:19 +02:00
fix(llm): alias tool names that collide with gpt-oss built-ins (#5878)
gpt-oss (harmony) ships BUILT-IN tools named python/browser, invoked with the raw body as the argument (to=python + bare source), while custom functions use to=functions.NAME + JSON. Exposing our own tools under those names makes the model answer with the built-in convention: it emits raw code, the server parses it as JSON, and the request dies with 'error parsing tool call: raw=import sys, ...'. In streaming mode Ollama does not report it at all — it truncates the stream, so the turn arrives as an empty response and the agent loop reads it as a model stall. bash collides the same way in practice. Measured on gpt-oss:20b via Ollama /v1, fixed agentic prompt, 12 runs per arm: python+bash as-is 2/12, python renamed 10/12, both renamed 12/12. 74 HTTP 500s were logged server-side during investigation with zero surfaced to the client. Rename the colliding tools on the outbound payload and map the names back on responses. Transport-only and gated on gpt-oss: every other model's schemas pass through untouched (asserted in tests). Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com>
This commit is contained in:
co-authored by
Claude Fable 5
Alexandre Teixeira
parent
d87a913729
commit
adfe3ab379
@@ -0,0 +1,78 @@
|
||||
"""Tools whose names collide with harmony built-ins must be aliased for gpt-oss.
|
||||
|
||||
gpt-oss (harmony format) ships BUILT-IN tools named `python` and `browser`,
|
||||
invoked with the raw body as the argument (`to=python` + bare source), while
|
||||
custom functions use `to=functions.NAME` + JSON. Exposing our own tool under a
|
||||
built-in's name makes the model answer with the built-in convention: it emits
|
||||
raw code, the server tries to parse it as JSON, and the request dies with
|
||||
"error parsing tool call: raw='import sys, ...'". Streaming is worse — Ollama
|
||||
truncates the stream instead of reporting it, so the turn looks like an empty
|
||||
response and the agent loop reads it as a stall.
|
||||
|
||||
Measured on gpt-oss:20b via Ollama /v1 with a fixed agentic prompt:
|
||||
python+bash as-is 2/6, python renamed 5/6, both renamed 6/6.
|
||||
|
||||
The aliasing is transport-only and gpt-oss-only: every other model's tool
|
||||
schemas must pass through untouched, and real tool names must come back out.
|
||||
"""
|
||||
from src.llm_core import (
|
||||
_alias_harmony_tools,
|
||||
_unalias_harmony_tool_name,
|
||||
_is_harmony_model,
|
||||
)
|
||||
|
||||
|
||||
def _tools(*names):
|
||||
return [
|
||||
{"type": "function", "function": {"name": n, "parameters": {}}}
|
||||
for n in names
|
||||
]
|
||||
|
||||
|
||||
def _names(tools):
|
||||
return [t["function"]["name"] for t in tools]
|
||||
|
||||
|
||||
def test_gpt_oss_colliding_names_are_aliased():
|
||||
out = _alias_harmony_tools(_tools("python", "bash", "web_search"), "gpt-oss:20b")
|
||||
assert _names(out) == ["run_python_code", "run_shell_command", "web_search"]
|
||||
|
||||
|
||||
def test_non_harmony_models_are_untouched():
|
||||
tools = _tools("python", "bash", "web_search")
|
||||
for model in ("qwen3-coder:30b", "gemma4:12b", "claude-opus-5", "gpt-4o", "llama-3.3"):
|
||||
out = _alias_harmony_tools(tools, model)
|
||||
assert _names(out) == ["python", "bash", "web_search"], model
|
||||
assert out is tools, f"{model} should get the same list object, not a copy"
|
||||
|
||||
|
||||
def test_aliasing_does_not_mutate_the_caller_list():
|
||||
tools = _tools("python")
|
||||
_alias_harmony_tools(tools, "gpt-oss:20b")
|
||||
assert _names(tools) == ["python"], "caller's schema list must not be mutated"
|
||||
|
||||
|
||||
def test_response_names_map_back_for_gpt_oss():
|
||||
assert _unalias_harmony_tool_name("run_python_code", "gpt-oss:20b") == "python"
|
||||
assert _unalias_harmony_tool_name("run_shell_command", "gpt-oss:20b") == "bash"
|
||||
# Unrelated names pass through untouched.
|
||||
assert _unalias_harmony_tool_name("web_search", "gpt-oss:20b") == "web_search"
|
||||
|
||||
|
||||
def test_response_names_untouched_for_other_models():
|
||||
# A non-harmony model that genuinely has a tool called run_python_code
|
||||
# must not have it rewritten to `python`.
|
||||
assert _unalias_harmony_tool_name("run_python_code", "qwen3-coder:30b") == "run_python_code"
|
||||
|
||||
|
||||
def test_harmony_detection():
|
||||
assert _is_harmony_model("gpt-oss:20b") is True
|
||||
assert _is_harmony_model("GPT-OSS:120B") is True
|
||||
assert _is_harmony_model("qwen3-coder:30b") is False
|
||||
assert _is_harmony_model("") is False
|
||||
assert _is_harmony_model(None) is False
|
||||
|
||||
|
||||
def test_empty_and_none_tools_are_safe():
|
||||
assert _alias_harmony_tools(None, "gpt-oss:20b") is None
|
||||
assert _alias_harmony_tools([], "gpt-oss:20b") == []
|
||||
Reference in New Issue
Block a user