agentcode-cli 1.2.2__tar.gz → 1.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentcode-cli
3
- Version: 1.2.2
3
+ Version: 1.3.1
4
4
  Summary: An open, multi-model agentic coding CLI — inspired by Claude Code
5
5
  Author: Vignesh Pai
6
6
  License: MIT License
@@ -18,7 +18,10 @@ import litellm
18
18
  from rich.console import Console
19
19
  from rich.markdown import Markdown
20
20
 
21
- from xml_tool_parser import looks_like_xml_tool_call, parse_xml_tool_calls, strip_think
21
+ from xml_tool_parser import (
22
+ looks_like_xml_tool_call, parse_xml_tool_calls, strip_think,
23
+ looks_like_json_tool_call, parse_json_tool_calls,
24
+ )
22
25
 
23
26
  from tools import TOOL_DEFINITIONS, execute_tool
24
27
  from router import ModelRouter, display_routing_decision
@@ -348,17 +351,19 @@ def run_agent_loop(
348
351
  cost,
349
352
  )
350
353
 
351
- # Open-weight fine-tunes (e.g. Vigp17/agentcode-27b) may emit tool
352
- # calls as inline XML in the text response. Detect and convert before
353
- # treating this as a pure text turn.
354
- if not tool_calls_accum and looks_like_xml_tool_call(full_text):
355
- cleaned, xml_calls = parse_xml_tool_calls(full_text)
356
- if xml_calls:
357
- full_text = cleaned
358
- for i, tc in enumerate(xml_calls):
359
- tool_calls_accum[i] = tc
360
- elif not tool_calls_accum:
361
- full_text = strip_think(full_text)
354
+ # Open-weight fine-tunes emit tool calls in the text response: the 27B
355
+ # uses XML (<function=...>), the 3B uses bare JSON. Detect and convert
356
+ # before treating this as a pure text turn.
357
+ if not tool_calls_accum:
358
+ if looks_like_xml_tool_call(full_text):
359
+ cleaned, parsed = parse_xml_tool_calls(full_text)
360
+ elif looks_like_json_tool_call(full_text):
361
+ cleaned, parsed = parse_json_tool_calls(full_text)
362
+ else:
363
+ cleaned, parsed = strip_think(full_text), []
364
+ full_text = cleaned
365
+ for i, tc in enumerate(parsed):
366
+ tool_calls_accum[i] = tc
362
367
 
363
368
  # Pure text response — done
364
369
  if not tool_calls_accum:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentcode-cli
3
- Version: 1.2.2
3
+ Version: 1.3.1
4
4
  Summary: An open, multi-model agentic coding CLI — inspired by Claude Code
5
5
  Author: Vignesh Pai
6
6
  License: MIT License
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentcode-cli"
3
- version = "1.2.2"
3
+ version = "1.3.1"
4
4
  description = "An open, multi-model agentic coding CLI — inspired by Claude Code"
5
5
  readme = "README.md"
6
6
  license = {file = "LICENSE"}
@@ -47,20 +47,11 @@ GEMINI_TIERS = [
47
47
  ModelTier("gemini/gemini-2.5-pro", "heavy", "Gemini 2.5 Pro", 1.25, 10.00),
48
48
  ]
49
49
 
50
- # Azure deployment names are user-defined, so tiers use common defaults.
51
- # Users can override via settings.json model.light/medium/heavy.
52
- AZURE_TIERS = [
53
- ModelTier("azure/gpt-4o-mini", "light", "Azure GPT-4o Mini", 0.15, 0.60),
54
- ModelTier("azure/gpt-4o", "medium", "Azure GPT-4o", 2.50, 10.00),
55
- ModelTier("azure/gpt-4o", "heavy", "Azure GPT-4o", 2.50, 10.00),
56
- ]
57
-
58
50
  # Provider configs keyed by prefix
59
51
  PROVIDER_TIERS = {
60
52
  "anthropic": ANTHROPIC_TIERS,
61
53
  "openai": OPENAI_TIERS,
62
54
  "gemini": GEMINI_TIERS,
63
- "azure": AZURE_TIERS,
64
55
  }
65
56
 
66
57
 
@@ -212,9 +203,7 @@ class ModelRouter:
212
203
  def detect_provider(self, model_string: str) -> str:
213
204
  """Detect provider from a model string."""
214
205
  m = model_string.lower()
215
- if m.startswith("azure/"):
216
- return "azure"
217
- elif "claude" in m or "anthropic" in m:
206
+ if "claude" in m or "anthropic" in m:
218
207
  return "anthropic"
219
208
  elif "gpt" in m or "openai" in m or "o1" in m or "o3" in m:
220
209
  return "openai"
@@ -12,7 +12,10 @@ from agent import (
12
12
  build_system_prompt, load_project_config, load_hooks,
13
13
  _run_subagents, _run_hook, _is_denied, _get_permission,
14
14
  )
15
- from xml_tool_parser import looks_like_xml_tool_call, parse_xml_tool_calls, strip_think
15
+ from xml_tool_parser import (
16
+ looks_like_xml_tool_call, parse_xml_tool_calls, strip_think,
17
+ looks_like_json_tool_call, parse_json_tool_calls,
18
+ )
16
19
 
17
20
 
18
21
  # ── Wire protocol ─────────────────────────────────────────────────────────────
@@ -107,17 +110,20 @@ def _server_turn(
107
110
  if tc.function.arguments:
108
111
  tool_calls_accum[idx]["arguments"] += tc.function.arguments
109
112
 
110
- # Some open-weight fine-tunes (e.g. Vigp17/agentcode-27b) emit tool
111
- # calls as inline XML in the text response instead of using litellm's
112
- # structured tool_calls field. Detect and convert.
113
- if not tool_calls_accum and looks_like_xml_tool_call(full_text):
114
- cleaned, xml_calls = parse_xml_tool_calls(full_text)
115
- if xml_calls:
116
- full_text = cleaned
117
- for i, tc in enumerate(xml_calls):
118
- tool_calls_accum[i] = tc
119
- elif not tool_calls_accum:
120
- full_text = strip_think(full_text)
113
+ # Open-weight fine-tunes emit tool calls in the text response instead of
114
+ # litellm's structured tool_calls field. The 27B uses XML
115
+ # (<function=...>), the 3B uses bare JSON ({"name":...,"arguments":...}).
116
+ # Detect and convert both.
117
+ if not tool_calls_accum:
118
+ if looks_like_xml_tool_call(full_text):
119
+ cleaned, parsed = parse_xml_tool_calls(full_text)
120
+ elif looks_like_json_tool_call(full_text):
121
+ cleaned, parsed = parse_json_tool_calls(full_text)
122
+ else:
123
+ cleaned, parsed = strip_think(full_text), []
124
+ full_text = cleaned
125
+ for i, tc in enumerate(parsed):
126
+ tool_calls_accum[i] = tc
121
127
 
122
128
  if router and usage:
123
129
  try:
@@ -85,3 +85,81 @@ def parse_xml_tool_calls(text: str) -> tuple[str, list[dict]]:
85
85
 
86
86
  cleaned = _TOOL_CALL_RE.sub("", cleaned).strip()
87
87
  return cleaned, tool_calls
88
+
89
+
90
+ # ── JSON-style tool calls (e.g. the AgentCode 3B) ─────────────────────────────
91
+ # Smaller fine-tunes often emit a bare JSON object instead of the XML form:
92
+ # {"name": "git_status", "arguments": {}}
93
+ # optionally wrapped in <tool_call>...</tool_call>. litellm doesn't pick these
94
+ # up as structured tool_calls, so we detect and convert them too.
95
+
96
+
97
+ def looks_like_json_tool_call(text: str) -> bool:
98
+ """Cheap check: a JSON object mentioning both name and arguments."""
99
+ return '"name"' in text and '"arguments"' in text
100
+
101
+
102
+ def _extract_json_objects(text: str) -> list[str]:
103
+ """Return top-level {...} substrings via brace matching (ignores braces in strings)."""
104
+ objs: list[str] = []
105
+ depth = 0
106
+ start = None
107
+ in_str = False
108
+ escape = False
109
+ for i, ch in enumerate(text):
110
+ if in_str:
111
+ if escape:
112
+ escape = False
113
+ elif ch == "\\":
114
+ escape = True
115
+ elif ch == '"':
116
+ in_str = False
117
+ continue
118
+ if ch == '"':
119
+ in_str = True
120
+ elif ch == "{":
121
+ if depth == 0:
122
+ start = i
123
+ depth += 1
124
+ elif ch == "}":
125
+ if depth > 0:
126
+ depth -= 1
127
+ if depth == 0 and start is not None:
128
+ objs.append(text[start:i + 1])
129
+ start = None
130
+ return objs
131
+
132
+
133
+ def parse_json_tool_calls(text: str) -> tuple[str, list[dict]]:
134
+ """
135
+ Extract bare-JSON tool calls and return (cleaned_text, tool_calls).
136
+
137
+ Handles {"name": ..., "arguments": {...}} objects, with or without
138
+ surrounding <tool_call> tags. Returns the same accumulator shape as
139
+ parse_xml_tool_calls.
140
+ """
141
+ cleaned = strip_think(text)
142
+ inner = cleaned.replace("<tool_call>", "").replace("</tool_call>", "")
143
+
144
+ tool_calls: list[dict] = []
145
+ matched_spans: list[str] = []
146
+ for cand in _extract_json_objects(inner):
147
+ try:
148
+ obj = json.loads(cand)
149
+ except (json.JSONDecodeError, ValueError):
150
+ continue
151
+ if isinstance(obj, dict) and "name" in obj and "arguments" in obj:
152
+ args = obj["arguments"]
153
+ args_str = args if isinstance(args, str) else json.dumps(args)
154
+ tool_calls.append({
155
+ "id": f"call_{uuid.uuid4().hex[:8]}",
156
+ "name": obj["name"],
157
+ "arguments": args_str,
158
+ })
159
+ matched_spans.append(cand)
160
+
161
+ if tool_calls:
162
+ for span in matched_spans:
163
+ cleaned = cleaned.replace(span, "")
164
+ cleaned = cleaned.replace("<tool_call>", "").replace("</tool_call>", "").strip()
165
+ return cleaned, tool_calls
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes