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
22 changes: 22 additions & 0 deletions slime/agent/adapters/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,28 @@ def json_arguments(value: Any) -> str:
return json.dumps(value, ensure_ascii=False)


def dict_arguments(value: Any) -> dict:
"""Tool-call arguments for chat-template rendering.

OpenAI wire responses carry ``function.arguments`` as a JSON string; Qwen-family
chat templates expect a mapping and iterate ``arguments.items()``. Decode echoed
wire strings back to dicts at the render boundary.
"""
if isinstance(value, dict):
return value
if isinstance(value, str):
try:
decoded = json.loads(value or "{}")
except json.JSONDecodeError:
decoded = value
if isinstance(decoded, dict):
return decoded
return {"_raw_arguments": decoded}
if value is None:
return {}
return {"_raw_arguments": value}


def render_token_ids(chain: AdapterChain, tokenizer) -> list[int]:
enc = tokenizer.apply_chat_template(
chain.chat_messages,
Expand Down
3 changes: 2 additions & 1 deletion slime/agent/adapters/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from slime.agent.adapters.common import ADAPTER_KEY, REASONING_PARSER_KEY, TOKENIZER_KEY, TOOL_PARSER_KEY
from slime.agent.adapters.common import AdapterChain as Chain
from slime.agent.adapters.common import BaseAdapter, call_sglang_generate
from slime.agent.adapters.common import dict_arguments as _dict_arguments
from slime.agent.adapters.common import json_arguments as _json_arguments
from slime.agent.adapters.common import ok_response, render_token_ids, request_session_id
from slime.agent.adapters.common import stable_hash as _hash
Expand Down Expand Up @@ -105,7 +106,7 @@ def _normalize_tool_call(call: dict[str, Any]) -> dict[str, Any]:
"type": "function",
"function": {
"name": name,
"arguments": _json_arguments(arguments),
"arguments": _dict_arguments(arguments),
},
}
if call.get("id"):
Expand Down
18 changes: 16 additions & 2 deletions tests/test_agent_adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
sys.path.insert(0, str(REPO_ROOT))

from slime.agent.adapters import anthropic, openai
from slime.agent.adapters.common import SGLANG_URL_KEY
from slime.agent.adapters.common import SGLANG_URL_KEY, dict_arguments
from slime.agent.trajectory import TurnRecord


Expand Down Expand Up @@ -167,6 +167,20 @@ def test_anthropic_translation_keeps_tool_results_and_tool_schema():
]


@pytest.mark.unit
def test_openai_render_tool_call_arguments_are_dicts():
# Echoed wire JSON string is decoded back to a mapping for chat-template rendering.
normalized = openai._normalize_tool_call({"function": {"name": "lookup", "arguments": '{"q": "slime"}'}})
assert normalized["function"]["arguments"] == {"q": "slime"}
# Valid-JSON-but-non-dict arguments are wrapped so ``arguments.items()`` stays safe.
wrapped = openai._normalize_tool_call({"function": {"name": "lookup", "arguments": "[1, 2]"}})
assert wrapped["function"]["arguments"] == {"_raw_arguments": [1, 2]}
# Falsy native non-dicts are still real argument values; only None means "no arguments".
assert dict_arguments(0) == {"_raw_arguments": 0}
assert dict_arguments([]) == {"_raw_arguments": []}
assert dict_arguments(None) == {}


@pytest.mark.unit
def test_openai_translation_and_responses_input_shapes():
chat_messages = openai._translate_chat_messages(
Expand Down Expand Up @@ -205,7 +219,7 @@ def test_openai_translation_and_responses_input_shapes():
{
"id": "call_1",
"type": "function",
"function": {"name": "lookup", "arguments": '{"q": "slime"}'},
"function": {"name": "lookup", "arguments": {"q": "slime"}},
}
],
},
Expand Down
Loading