File size: 4,390 Bytes
6464112
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
"""End-to-end pure-Python regression for the OpenClaude tool wire contract."""

from __future__ import annotations

import json
import unittest

from openai_compat import (
    analyze_tool_flow,
    indexed_tool_calls,
    resolve_tool_choice,
    select_tools,
    tool_names,
)
from openclaude_compat import normalize_openclaude_messages
from tool_calls import extract_tool_calls


GLOB = {
    "type": "function",
    "function": {
        "name": "Glob",
        "description": "Find files by glob pattern.",
        "parameters": {
            "type": "object",
            "properties": {
                "pattern": {"type": "string"},
                "path": {"type": "string"},
            },
            "required": ["pattern"],
        },
    },
}
READ = {
    "type": "function",
    "function": {
        "name": "Read",
        "description": "Read a file.",
        "parameters": {
            "type": "object",
            "properties": {"file_path": {"type": "string"}},
            "required": ["file_path"],
        },
    },
}


class OpenClaudeToolContractTests(unittest.TestCase):
    def test_repository_summary_becomes_structured_openai_tool_call(self) -> None:
        # This is the public failure shape reported with Qwen coder models:
        # OpenClaude asks for a repository summary with tool_choice=auto.
        messages = [
            {"role": "user", "content": "Summarize this repository structure."}
        ]
        tools = [GLOB, READ]

        state = analyze_tool_flow(messages, tools)
        choice = resolve_tool_choice("auto", state)
        selected, mode = select_tools(tools, choice)

        self.assertEqual(mode, "forced")
        self.assertEqual([t["function"]["name"] for t in selected], ["Glob"])

        # Exact Qwen2.5-Coder native function-call syntax.
        model_text = (
            '<tool_call>{"name":"Glob","arguments":{"pattern":"**/*"}}'
            "</tool_call>"
        )
        calls, visible = extract_tool_calls(model_text, tool_names(selected))
        self.assertEqual(visible, "")
        self.assertEqual(len(calls), 1)
        self.assertEqual(calls[0]["type"], "function")
        self.assertEqual(calls[0]["function"]["name"], "Glob")
        self.assertEqual(
            json.loads(calls[0]["function"]["arguments"]),
            {"pattern": "**/*"},
        )

        # OpenClaude's streaming converter requires a stable `index`, while the
        # non-streaming converter consumes the same id/name/arguments payload.
        streamed = indexed_tool_calls(calls)
        self.assertEqual(streamed[0]["index"], 0)
        self.assertTrue(streamed[0]["id"].startswith("call_"))

    def test_tool_result_round_trip_preserves_call_id_and_arguments_mapping(self) -> None:
        history = [
            {"role": "user", "content": "Summarize this repository structure."},
            {
                "role": "assistant",
                "content": None,
                "tool_calls": [
                    {
                        "id": "call_contract_1",
                        "type": "function",
                        "function": {
                            "name": "Glob",
                            "arguments": '{"pattern":"**/*"}',
                        },
                    }
                ],
            },
            {
                "role": "tool",
                "tool_call_id": "call_contract_1",
                "name": "Glob",
                "content": "app.py\nopenai_compat.py\ntool_calls.py",
            },
        ]
        normalized = normalize_openclaude_messages(history)
        assistant = next(m for m in normalized if m["role"] == "assistant")
        result = next(m for m in normalized if m["role"] == "tool")

        call = assistant["tool_calls"][0]
        self.assertEqual(call["id"], "call_contract_1")
        self.assertEqual(call["function"]["arguments"], {"pattern": "**/*"})
        self.assertEqual(result["tool_call_id"], "call_contract_1")
        self.assertEqual(result["name"], "Glob")

        followup_state = analyze_tool_flow(history, [GLOB, READ])
        # After evidence exists, auto stays available instead of being poisoned
        # by the first turn. The model may summarize or request another tool.
        self.assertEqual(resolve_tool_choice("auto", followup_state), "auto")


if __name__ == "__main__":
    unittest.main()