pi-tool-repair 0.1.0
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.
- package/README.md +237 -0
- package/docs/tool-call-grammar-leakage-survey.md +289 -0
- package/package.json +56 -0
- package/src/grammar-repair.ts +980 -0
- package/src/index.ts +509 -0
- package/tool-repair.ts +192 -0
- package/vitest.config.ts +15 -0
package/README.md
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# π§ pi-tool-repair
|
|
4
|
+
|
|
5
|
+
**Validate-then-repair for [pi](https://github.com/earendil-works/pi-coding-agent)**
|
|
6
|
+
|
|
7
|
+
_Fixes the finite set of tool-call mistakes open models make β before tools execute._
|
|
8
|
+
|
|
9
|
+
[](https://github.com/earendil-works/pi-coding-agent)
|
|
10
|
+
[](./LICENSE)
|
|
11
|
+
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
> **Open models aren't bad at tool calling β the harness is.**
|
|
17
|
+
>
|
|
18
|
+
> By adding a thin repair layer, DeepSeek V4 Pro beat Opus 4.7 in 6/10 internal evals β without changing the model. The same four mistakes repeat across DeepSeek, GLM, Qwen, and others. Each fix is 30β100 lines. Order matters.
|
|
19
|
+
|
|
20
|
+
Reverse-engineered from [Command Code](https://commandcode.ai/)'s tool parsing pipeline.
|
|
21
|
+
|
|
22
|
+
## What it fixes
|
|
23
|
+
|
|
24
|
+
| Problem | Model sends | After repair |
|
|
25
|
+
| ----------------------------- | ---------------------------------- | --------------------------- |
|
|
26
|
+
| `null` for optional fields | `{"path": "/foo", "offset": null}` | `{"path": "/foo"}` |
|
|
27
|
+
| Arrays as JSON strings | `"[\"a\",\"b\"]"` | `["a","b"]` |
|
|
28
|
+
| `{}` where array expected | `{"include": {}}` | _(dropped)_ |
|
|
29
|
+
| Bare string β array | `"foo"` | `["foo"]` |
|
|
30
|
+
| Wrong field names | `{"file_path": "/foo"}` | `{"path": "/foo"}` |
|
|
31
|
+
| Bare string as root input | `"/path/to/file"` | `{"path": "/path/to/file"}` |
|
|
32
|
+
| Schema anchor bleed (Kimi K2) | `"^pattern$"` in values | `"pattern"` |
|
|
33
|
+
| Leaked tool grammar (opt-in) | `<ο½DSMLο½tool_calls>...` | pi `toolCall` block |
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
**With `pi install`** (recommended):
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pi install https://github.com/monotykamary/pi-tool-repair
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Manual** β add to `~/.pi/agent/settings.json`:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"packages": ["git:github.com/monotykamary/pi-tool-repair"]
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Local development** β add the extension path directly:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"extensions": ["./path/to/pi-tool-repair/tool-repair.ts"]
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Reload with `/reload` after any install method.
|
|
60
|
+
|
|
61
|
+
## How it works
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
65
|
+
β Phase 0: Schema poisoning (before_provider_request) β
|
|
66
|
+
β β
|
|
67
|
+
β Strip regex anchors from JSON Schema patterns for models β
|
|
68
|
+
β where they leak into generated values (Kimi K2, MiniMax) β
|
|
69
|
+
β β
|
|
70
|
+
β Fixes what YOU send the model β not what the model sends β
|
|
71
|
+
ββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββ
|
|
72
|
+
β
|
|
73
|
+
βΌ
|
|
74
|
+
Model generates tool call
|
|
75
|
+
β
|
|
76
|
+
βΌ
|
|
77
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
78
|
+
β Phase 1: Grammar leak repair (message_end, opt-in) β
|
|
79
|
+
β β
|
|
80
|
+
β Detect raw XML/sentinel tool grammars emitted as text or β
|
|
81
|
+
β thinking, strip them from visible output, and recover them β
|
|
82
|
+
β as pi toolCall blocks when complete and safe. β
|
|
83
|
+
ββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββ
|
|
84
|
+
β
|
|
85
|
+
βΌ
|
|
86
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
87
|
+
β Phase 2: Validate-then-repair (tool_call) β
|
|
88
|
+
β β
|
|
89
|
+
β 1. Validate input against schema (if known tool) β
|
|
90
|
+
β β³ Valid? Ship it untouched. β
|
|
91
|
+
β 2. Walk the validator's issue list β
|
|
92
|
+
β β³ Apply targeted repairs only at the exact failed paths β
|
|
93
|
+
β 3. Re-validate the repaired input β
|
|
94
|
+
β β³ Still invalid? Let the tool handle it. β
|
|
95
|
+
β 4. Log outcome (debug mode) β
|
|
96
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Repair rules (in order)
|
|
100
|
+
|
|
101
|
+
Order matters β `parseJsonStringifiedArray` must run before `wrapBareStringAsArray` or you get double-wrapping.
|
|
102
|
+
|
|
103
|
+
| # | Rule | What it catches |
|
|
104
|
+
| --- | ---------------------------- | ----------------------------------------------- |
|
|
105
|
+
| 1 | `renameAliasedField` | `file_path` β `path`, `query` β `pattern`, etc. |
|
|
106
|
+
| 2 | `dropNullOrUndefined` | `null`/`undefined` for optional fields |
|
|
107
|
+
| 3 | `dropEmptyObjectPlaceholder` | `{}` where array expected |
|
|
108
|
+
| 4 | `parseJsonStringifiedArray` | `"[\"a\",\"b\"]"` β `["a","b"]` |
|
|
109
|
+
| 5 | `wrapBareStringAsArray` | `"foo"` β `["foo"]` |
|
|
110
|
+
| 6 | `wrapRootStringAsObject` | `"/path"` β `{"path": "/path"}` |
|
|
111
|
+
|
|
112
|
+
### Why validate-then-repair (not preprocess-then-validate)
|
|
113
|
+
|
|
114
|
+
Preprocessing inputs before validation silently corrupts valid data β rewriting file content that happened to look like JSON, for example. The better design:
|
|
115
|
+
|
|
116
|
+
1. **Parse the input as-is.** If valid, ship it untouched.
|
|
117
|
+
2. **On failure, walk the validator's issue list** and apply repairs only at the exact paths that failed.
|
|
118
|
+
3. **Re-validate.** The schema localizes the bug for you β you only spend repair effort where it's actually needed.
|
|
119
|
+
|
|
120
|
+
## Configuration
|
|
121
|
+
|
|
122
|
+
### Grammar leak repair (disabled by default)
|
|
123
|
+
|
|
124
|
+
Raw XML/sentinel tool-call grammar recovery is opt-in because it can turn assistant text into tool execution. Enable it in `~/.pi/agent/extensions/pi-tool-repair.json`:
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{
|
|
128
|
+
"grammarRepair": {
|
|
129
|
+
"enabled": true,
|
|
130
|
+
"mode": "recover",
|
|
131
|
+
"requireKnownTool": true,
|
|
132
|
+
"grammars": [
|
|
133
|
+
"dsml",
|
|
134
|
+
"invoke",
|
|
135
|
+
"qwen",
|
|
136
|
+
"kimi",
|
|
137
|
+
"mistral",
|
|
138
|
+
"llama",
|
|
139
|
+
"glm",
|
|
140
|
+
"granite",
|
|
141
|
+
"minimax-text",
|
|
142
|
+
"olmo"
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Modes:
|
|
149
|
+
|
|
150
|
+
| Mode | Behavior |
|
|
151
|
+
| --------- | -------------------------------------------------------------- |
|
|
152
|
+
| `recover` | Strip leaked markup and append recovered pi `toolCall` blocks. |
|
|
153
|
+
| `strip` | Strip leaked markup only; do not execute recovered calls. |
|
|
154
|
+
|
|
155
|
+
Safety gates:
|
|
156
|
+
|
|
157
|
+
- `requireKnownTool: true` only recovers calls whose name is in pi's active tool registry.
|
|
158
|
+
- Markup inside fenced code blocks is ignored so syntax discussions and examples are preserved.
|
|
159
|
+
- Incomplete or unparseable blocks are left alone.
|
|
160
|
+
- If the provider already emitted native `toolCall` blocks, leaked shadow text is stripped but duplicate calls are not added.
|
|
161
|
+
|
|
162
|
+
Covered grammar families: DeepSeek DSML, MiniMax/Anthropic `<invoke>`, Qwen/Hermes `<tool_call>`, Kimi sentinels, Mistral `[TOOL_CALLS]`, Llama `<|python_tag|>`, GLM `arg_key`/`arg_value`, Granite JSON `<tool_call>`, MiniMax-Text-01 TypeScript calls, and OLMo3 `<function_calls>` pythonic calls. See [`docs/tool-call-grammar-leakage-survey.md`](./docs/tool-call-grammar-leakage-survey.md) for the survey.
|
|
163
|
+
|
|
164
|
+
### Debug logging
|
|
165
|
+
|
|
166
|
+
Set `PI_TOOL_REPAIR_DEBUG=1` or `grammarRepair.debug: true` to log repair diagnostics to stderr:
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
[pi-tool-repair] tool=read outcome=recovered rules=dropNullOrUndefined hints=1
|
|
170
|
+
input: {"path":"/foo","offset":null}
|
|
171
|
+
repaired: {"path":"/foo"}
|
|
172
|
+
hint[0]: Dropped null `offset` from tool "read"...
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Covered tools
|
|
176
|
+
|
|
177
|
+
Repair rules apply to pi's built-in tools: `read`, `write`, `edit`, `bash`, `grep`, `find`, `ls`.
|
|
178
|
+
|
|
179
|
+
### Anchor bleed models
|
|
180
|
+
|
|
181
|
+
Phase 0 schema sanitization activates for models matching these patterns:
|
|
182
|
+
|
|
183
|
+
| Pattern | Models |
|
|
184
|
+
| ------------ | ---------------- |
|
|
185
|
+
| `/kimi-k2/i` | Kimi K2 variants |
|
|
186
|
+
| `/minimax/i` | MiniMax variants |
|
|
187
|
+
| `/glm/i` | GLM variants |
|
|
188
|
+
|
|
189
|
+
To add more models, edit `anchorBleedModels` in [`src/index.ts`](./src/index.ts).
|
|
190
|
+
|
|
191
|
+
### Field aliases
|
|
192
|
+
|
|
193
|
+
The extension maps common model mistakes (wrong field names) to the canonical field name. For example, when calling `read`, the model can send `file_path`, `absolutePath`, `filepath`, `target_file`, etc. β all map to `path`.
|
|
194
|
+
|
|
195
|
+
<details>
|
|
196
|
+
<summary><strong>Full alias table</strong></summary>
|
|
197
|
+
|
|
198
|
+
| Tool | Canonical | Aliases |
|
|
199
|
+
| ------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
200
|
+
| `read` | `path` | `absolutePath`, `file_path`, `filePath`, `filepath`, `pathname`, `target_file`, `targetFile`, `file`, `absolute_path`, `fileAbsolutePath` |
|
|
201
|
+
| `grep` | `pattern` | `query`, `regex`, `search`, `q`, `expression`, `text` |
|
|
202
|
+
| `write` | `path` | `absolutePath`, `file_path`, `filePath`, `filepath`, `pathname`, `target_file`, `targetFile` |
|
|
203
|
+
| `write` | `content` | `text`, `body`, `data`, `contents`, `fileContent` |
|
|
204
|
+
| `edit` | `path` | `absolutePath`, `file_path`, `filePath`, `filepath`, `pathname`, `target_file`, `targetFile` |
|
|
205
|
+
| `edit` | `oldText` | `old_string`, `oldString`, `old`, `old_str`, `oldStr`, `from`, `old_value`, `oldText`, `old_text`, `oldContent`, `old_content` |
|
|
206
|
+
| `edit` | `newText` | `new_string`, `newString`, `new`, `new_str`, `newStr`, `to`, `new_value`, `newText`, `new_text`, `newContent`, `new_content` |
|
|
207
|
+
| `ls` | `path` | `absolutePath`, `directory`, `dir`, `folder`, `directoryPath` |
|
|
208
|
+
| `find` | `pattern` | `query`, `glob`, `expression`, `search`, `include` |
|
|
209
|
+
| `bash` | `command` | `cmd`, `shell`, `script`, `commandLine` |
|
|
210
|
+
|
|
211
|
+
</details>
|
|
212
|
+
|
|
213
|
+
## Development
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
npm install
|
|
217
|
+
npm test # run tests
|
|
218
|
+
npm run test:watch # watch mode
|
|
219
|
+
npm run test:coverage # coverage report
|
|
220
|
+
npm run typecheck # type checking
|
|
221
|
+
npm run lint:dead # dead code detection
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Related projects
|
|
225
|
+
|
|
226
|
+
| Project | Description |
|
|
227
|
+
| ------------------------------------------------------------------------------ | ----------------------------------------------------------- |
|
|
228
|
+
| [pi-retry](https://github.com/monotykamary/pi-retry) | Automatic retry for 400/413/connection errors |
|
|
229
|
+
| [pi-fast-resume](https://github.com/monotykamary/pi-fast-resume) | Instant session picker (6ms vs 5.6s) |
|
|
230
|
+
| [pi-hide-providers](https://github.com/monotykamary/pi-hide-providers) | Hide providers and models from the selector |
|
|
231
|
+
| [pi-double-esc](https://github.com/monotykamary/pi-double-esc) | Prevent accidental Escape aborts |
|
|
232
|
+
| [pi-loop](https://github.com/monotykamary/pi-loop) | Close the verification loop on task completion |
|
|
233
|
+
| [pi-fireworks-provider](https://github.com/monotykamary/pi-fireworks-provider) | Fireworks AI provider (origin of the Kimi anchor-bleed fix) |
|
|
234
|
+
|
|
235
|
+
## License
|
|
236
|
+
|
|
237
|
+
[MIT](./LICENSE)
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# Tool-Call Grammar Leakage Survey (June 2026)
|
|
2
|
+
|
|
3
|
+
Survey of LLMs that use XML-like / sentinel-token tool-call grammars and are
|
|
4
|
+
observed to *leak* the raw markup into the user-visible `content` (or
|
|
5
|
+
`reasoning_content`) field instead of emitting structured `tool_calls`.
|
|
6
|
+
|
|
7
|
+
This is the input for an opt-in `pi-tool-repair` feature: a configurable,
|
|
8
|
+
**disabled-by-default** flag (set in `~/.pi/agent/extensions/pi-tool-repair.json`)
|
|
9
|
+
that recovers leaked grammar back into structured tool calls β or at minimum
|
|
10
|
+
strips it from visible output.
|
|
11
|
+
|
|
12
|
+
## Why leakage happens (root causes, consistent across families)
|
|
13
|
+
|
|
14
|
+
The same handful of mechanisms produce leakage regardless of vendor:
|
|
15
|
+
|
|
16
|
+
1. **Reasoning/tool-parser ordering.** When both a reasoning parser and a
|
|
17
|
+
tool-call parser are enabled, the tool parser only runs on `content` after
|
|
18
|
+
the reasoning parser yields. If the model emits the tool grammar *inside* an
|
|
19
|
+
unclosed `<think>`/reasoning block, the tags land in `reasoning_content` and
|
|
20
|
+
the tool parser never sees them. (DeepSeek, Qwen3.5/3.6, Kimi K2.)
|
|
21
|
+
2. **Split start-marker across stream chunks.** Long sentinel start tokens
|
|
22
|
+
(`<ο½ο½DSMLο½ο½tool_calls>`, `<|tool_calls_section_begin|>`) get split across
|
|
23
|
+
SSE chunks; the parser emits the partial prefix as plain text before it can
|
|
24
|
+
recognize the marker. (DeepSeek DSML, Kimi K2, MiniMax.)
|
|
25
|
+
3. **`tool_choice=auto` + `stream=true`** is the high-risk path. `required` and
|
|
26
|
+
`stream=false` are markedly more stable because they take a different
|
|
27
|
+
(constrained) decode/parse branch.
|
|
28
|
+
4. **Tokenizer `skip_special_tokens=True` in batched decode.** When a tool-call
|
|
29
|
+
request shares a batch with a non-tool request, the sentinel/special tokens
|
|
30
|
+
get stripped, so the marker (`ο½DSMLο½`, `[TOOL_CALLS]`) is simply *missing*
|
|
31
|
+
and downstream parsers fail. (DeepSeek V3.2, Mistral.)
|
|
32
|
+
5. **Provider/engine parser absent or mismatched.** Self-hosted vLLM/SGLang/MLX
|
|
33
|
+
or aggregators (OpenRouter, Novita, Fireworks, NVIDIA, OpenCode Zen,
|
|
34
|
+
Tensorix, Foundry) ship the model before a correct `--tool-call-parser`
|
|
35
|
+
exists or with the wrong one, so raw markup passes straight through.
|
|
36
|
+
6. **Higher temperature** increases format drift away from the strict grammar.
|
|
37
|
+
|
|
38
|
+
Prompt-level mitigations ("do not output DSML tags") are unreliable: emission is
|
|
39
|
+
a tokenizer/training-level pattern, not instruction-following.
|
|
40
|
+
|
|
41
|
+
## The grammar families to handle
|
|
42
|
+
|
|
43
|
+
These are the concrete shapes that show up in leaked `content`. Group them by
|
|
44
|
+
parser, not by vendor β multiple vendors share a shape.
|
|
45
|
+
|
|
46
|
+
### A. DeepSeek "DSML" XML (`ο½DSMLο½`, U+FF5C fullwidth bars)
|
|
47
|
+
|
|
48
|
+
DeepSeek V3.2 / V4 (V4-Pro, V4-Flash). The `ο½` is U+FF5C fullwidth vertical bar.
|
|
49
|
+
Three observed bar variants plus an ASCII-pipe variant from some proxies:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
# Double-bar (most common in live V4 SSE)
|
|
53
|
+
<ο½ο½DSMLο½ο½tool_calls>
|
|
54
|
+
<ο½ο½DSMLο½ο½invoke name="code_exec">
|
|
55
|
+
<ο½ο½DSMLο½ο½parameter name="language" string="true">python</ο½ο½DSMLο½ο½parameter>
|
|
56
|
+
</ο½ο½DSMLο½ο½invoke>
|
|
57
|
+
</ο½ο½DSMLο½ο½tool_calls>
|
|
58
|
+
|
|
59
|
+
# Single-bar
|
|
60
|
+
<ο½DSMLο½tool_calls>
|
|
61
|
+
<ο½DSMLο½invoke name="fetch">
|
|
62
|
+
<ο½DSMLο½parameter name="url" string="false">["x"]</ο½DSMLο½parameter>
|
|
63
|
+
</ο½DSMLο½invoke>
|
|
64
|
+
</ο½DSMLο½tool_calls>
|
|
65
|
+
|
|
66
|
+
# No-leading-bar
|
|
67
|
+
<DSMLο½tool_calls>
|
|
68
|
+
<DSMLο½invoke name="bash">
|
|
69
|
+
</DSMLο½invoke>
|
|
70
|
+
|
|
71
|
+
# ASCII-pipe variant (proxies/aggregators that transcode the fullwidth bars)
|
|
72
|
+
< | DSML | tool_calls>
|
|
73
|
+
< | DSML | invoke name="builtin_web_search">
|
|
74
|
+
< | DSML | parameter name="additionalContext" string="true">...</ | DSML | parameter>
|
|
75
|
+
</ | DSML | invoke>
|
|
76
|
+
</ | DSML | tool_calls>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Key details:
|
|
80
|
+
- `function_calls` vs `tool_calls` both appear as the outer wrapper (V3.2 used
|
|
81
|
+
`function_calls`; V4 uses `tool_calls`). Handle both, singular and plural.
|
|
82
|
+
- `string="true"` β value passed as-is; `string="false"` β value is JSON
|
|
83
|
+
(number/bool/array/object). The repair must respect this when building args.
|
|
84
|
+
- Inner body can be XML `<parameter>` tags **or** a direct JSON object.
|
|
85
|
+
- Newlines inside `<parameter>` values are common (multi-line bash) and break
|
|
86
|
+
naive non-DOTALL matching β a real failure mode reported against pi itself
|
|
87
|
+
(earendil-works/pi#3712).
|
|
88
|
+
- Providers with reported leakage: official DeepSeek API, **NVIDIA**,
|
|
89
|
+
**Novita**, **Fireworks**, **OpenCode Zen**, **Tensorix**, Microsoft Foundry,
|
|
90
|
+
CherryIN, self-hosted vLLM/SGLang/antirez-ds4.
|
|
91
|
+
|
|
92
|
+
### B. Anthropic-style `<invoke>` / `<parameter>` XML (no DSML bars)
|
|
93
|
+
|
|
94
|
+
This is the canonical Anthropic tool-use shape, widely copied. Leaked by:
|
|
95
|
+
|
|
96
|
+
- **MiniMax** M1, M2.1, M2.5, M2.7, M3 β wrapped in `<minimax:tool_call>`:
|
|
97
|
+
```xml
|
|
98
|
+
<minimax:tool_call>
|
|
99
|
+
<invoke name="search_web">
|
|
100
|
+
<parameter name="query_list">["..."]</parameter>
|
|
101
|
+
</invoke>
|
|
102
|
+
</minimax:tool_call>
|
|
103
|
+
```
|
|
104
|
+
MiniMax M2 also has a *malformed* variant that drops the `<`/`</` angle
|
|
105
|
+
brackets entirely (`invoke name="x">` β¦ `parameter name="y">val parameter>`),
|
|
106
|
+
needing bracket-repair before XML parsing.
|
|
107
|
+
- **Anthropic Claude via OpenRouter** occasionally emits raw `<tool_call>` /
|
|
108
|
+
`<tool_result>` tags as text.
|
|
109
|
+
- Generic `<tool_call>...</tool_call>` and `<tool_result>...</tool_result>`
|
|
110
|
+
blocks leaked across many OpenAI-compat providers.
|
|
111
|
+
|
|
112
|
+
### C. Qwen / Hermes XML (`<tool_call>` + `<function=β¦>` + `<parameter=β¦>`)
|
|
113
|
+
|
|
114
|
+
Two sub-shapes β note the `=` (Qwen3-Coder/qwen3_xml) vs `name="β¦"` (Hermes):
|
|
115
|
+
|
|
116
|
+
```xml
|
|
117
|
+
# Qwen3-Coder / qwen3_xml shape (attribute via '=')
|
|
118
|
+
<tool_call>
|
|
119
|
+
<function=get_weather>
|
|
120
|
+
<parameter=location>Paris</parameter>
|
|
121
|
+
</function>
|
|
122
|
+
</tool_call>
|
|
123
|
+
|
|
124
|
+
# Hermes shape (JSON inside <tool_call>)
|
|
125
|
+
<tool_call>
|
|
126
|
+
{"name": "get_weather", "arguments": {"location": "Paris"}}
|
|
127
|
+
</tool_call>
|
|
128
|
+
|
|
129
|
+
# Qwen2.5-Coder oddball: wraps in <tools> with a JSON body
|
|
130
|
+
<tools>
|
|
131
|
+
{"name": "get_weather", "arguments": {"location": "San Francisco, CA"}}
|
|
132
|
+
</tools>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Affected: Qwen2.5 / Qwen2.5-Coder / Qwen2.5-VL, QwQ-32B, Qwen3 / Qwen3-Coder
|
|
136
|
+
(30B, 480B, Next), **Qwen3.5** (9B/35B-A3B most unstable), **Qwen3.6**
|
|
137
|
+
(27B / 35B-A3B). Common failure: XML lands in `reasoning_content` when emitted
|
|
138
|
+
inside an unclosed `<think>`. Also malformed variants (merged tags
|
|
139
|
+
`<function=edit>` missing `<`, bare `<function>` without `<tool_call>`,
|
|
140
|
+
mismatched/missing closers) β see the community `qwen-toolcall-fixer` proxy.
|
|
141
|
+
|
|
142
|
+
### D. Kimi K2 sentinel tokens (`<|tool_calls_section_begin|>` β¦)
|
|
143
|
+
|
|
144
|
+
Moonshot Kimi K2 / K2-Thinking / K2.6:
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
<|tool_calls_section_begin|>
|
|
148
|
+
<|tool_call_begin|>functions.web_search:0<|tool_call_argument_begin|>{"query": "..."}<|tool_call_end|>
|
|
149
|
+
<|tool_calls_section_end|>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Details:
|
|
153
|
+
- Singular variants also exist: `<|tool_call_section_begin|>` / `_end`.
|
|
154
|
+
- Tool-call ID format is load-bearing: `functions.{name}:{idx}`. Bare
|
|
155
|
+
`call00003` / `search:2` / `call_0001` (OpenAI-style) confuse the model and
|
|
156
|
+
the parser, causing leakage or empty `tool_calls`. Hosted Moonshot API
|
|
157
|
+
normalizes IDs; OSS deployments must normalize client-side.
|
|
158
|
+
- Leaks into `reasoning_delta`/`reasoning_content` in thinking mode.
|
|
159
|
+
- Sparse toolsets (1β2 tools) increase the inline-token leakage rate
|
|
160
|
+
(observed via OpenRouter for `moonshotai/kimi-k2.6`).
|
|
161
|
+
|
|
162
|
+
### E. Mistral sentinel tokens (`[TOOL_CALLS]`, `[ARGS]`, `[CALL_ID]`)
|
|
163
|
+
|
|
164
|
+
Mistral 7B v0.3, Nemo, Ministral-8B, Mistral-Large-2407, plus V7/V11 tokenizers:
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
[TOOL_CALLS] [{"name": "calculator", "arguments": {"operation": "2+2"}, "id": "VvvODy9mT"}]
|
|
168
|
+
# V11: [TOOL_CALLS]name[CALL_ID]id[ARGS]{...}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Failure modes: tool call emitted as plain JSON text (no `[TOOL_CALLS]` token),
|
|
172
|
+
`skip_special_tokens` dropping the sentinel, brittle behavior above temperature
|
|
173
|
+
0, IDs must be 9-char alphanumeric.
|
|
174
|
+
|
|
175
|
+
### F. Llama `<|python_tag|>` + JSON / pythonic
|
|
176
|
+
|
|
177
|
+
Llama 3.1 / 3.2 / 3.3 / 4. Tool call prefixed with `<|python_tag|>`, body is
|
|
178
|
+
JSON (`llama3_json` / `llama4_json`) or pythonic (`pythonic` / `llama4_pythonic`).
|
|
179
|
+
Leaks when the `<|python_tag|>` token is dropped or the model wraps JSON in
|
|
180
|
+
markdown / adds preamble text. Llama 3.2 emits *no* start token at all, which is
|
|
181
|
+
its own leakage class.
|
|
182
|
+
|
|
183
|
+
### G. GLM `<tool_call>` + `<arg_key>` / `<arg_value>` XML
|
|
184
|
+
|
|
185
|
+
Zhipu/Z.ai GLM-4.5 / GLM-4.6 / GLM-4.7 (`glm4_moe` / `glm47` parser), and
|
|
186
|
+
GLM-5:
|
|
187
|
+
|
|
188
|
+
```xml
|
|
189
|
+
<tool_call>get_weather
|
|
190
|
+
<arg_key>city</arg_key>
|
|
191
|
+
<arg_value>Beijing</arg_value>
|
|
192
|
+
</tool_call>
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Reasoning wrapped in `<think>`/`</think>`. Zero-arg calls
|
|
196
|
+
(`<tool_call>get_current_date</tool_call>`) and same-line name+args are valid
|
|
197
|
+
variants. GLM-5 observed leaking raw `<tool_call>` shadow text to chat channels.
|
|
198
|
+
|
|
199
|
+
### H. IBM Granite `<tool_call>` + JSON (Hermes-derived)
|
|
200
|
+
|
|
201
|
+
Granite 4.0 / 4.1 (and Nano). Uses Hermes convention:
|
|
202
|
+
`<tool_call>{"name": β¦, "arguments": β¦}</tool_call>`. Known quirk: emits
|
|
203
|
+
arguments as an escaped *string* instead of JSON object. Granite 3.3 / 4.0
|
|
204
|
+
H-Small emit *pythonic* calls (`func(kw=val)`) instead β a separate shape.
|
|
205
|
+
|
|
206
|
+
### I. MiniMax-Text-01 typescript-style
|
|
207
|
+
|
|
208
|
+
Older MiniMax text model emits:
|
|
209
|
+
```
|
|
210
|
+
<function_call>```typescript
|
|
211
|
+
functions.get_current_weather({"location": "Shanghai"})
|
|
212
|
+
```
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### J. OLMo 3 `<function_calls>` + pythonic
|
|
216
|
+
|
|
217
|
+
AllenAI OLMo 3 Instruct: newline-delimited pythonic calls wrapped in
|
|
218
|
+
`<function_calls>...</function_calls>` (allows JSON `true`/`false`/`null`).
|
|
219
|
+
(OLMo 3 *Think* models are not trained for tools β leakage there is the model
|
|
220
|
+
narrating, not a grammar.)
|
|
221
|
+
|
|
222
|
+
## Quick reference table
|
|
223
|
+
|
|
224
|
+
| Family | Models | Marker shape | Inner body | Leaks into |
|
|
225
|
+
|---|---|---|---|---|
|
|
226
|
+
| A. DSML | DeepSeek V3.2, V4-Pro, V4-Flash | `<ο½DSMLο½tool_calls>` / `function_calls` (+ double-bar, no-bar, ASCII `< | DSML |`) | `<parameter name= string=>` or JSON | content + reasoning |
|
|
227
|
+
| B. invoke/parameter | MiniMax M1βM3, Claude (via OpenRouter) | `<minimax:tool_call>` / `<invoke name=>` `<parameter name=>` | text / JSON | content |
|
|
228
|
+
| C. Qwen/Hermes | Qwen2.5/3/3.5/3.6, Coder, VL, QwQ | `<tool_call>` + `<function=>` `<parameter=>` or JSON | text / JSON | reasoning_content |
|
|
229
|
+
| D. Kimi sentinels | Kimi K2 / K2-Thinking / K2.6 | `<\|tool_calls_section_begin\|>` β¦ | `functions.name:idx` + JSON | content + reasoning_delta |
|
|
230
|
+
| E. Mistral sentinels | 7B v0.3, Nemo, Ministral, Large-2407 | `[TOOL_CALLS]` `[ARGS]` `[CALL_ID]` | JSON array | content |
|
|
231
|
+
| F. Llama python_tag | Llama 3.1/3.2/3.3/4 | `<\|python_tag\|>` (or none) | JSON / pythonic | content |
|
|
232
|
+
| G. GLM arg_key | GLM-4.5/4.6/4.7, GLM-5 | `<tool_call>name` + `<arg_key>` `<arg_value>` | XML kv | content + think |
|
|
233
|
+
| H. Granite | Granite 4.0/4.1 (+ Nano) | `<tool_call>` | JSON (sometimes escaped string) | content |
|
|
234
|
+
| I. MiniMax-Text-01 | MiniMax-Text-01 | `<function_call>` ```typescript | `functions.x({...})` | content |
|
|
235
|
+
| J. OLMo3 | OLMo 3 Instruct | `<function_calls>` | newline pythonic | content |
|
|
236
|
+
|
|
237
|
+
## Provider hot-spots (where leakage is reported in the wild)
|
|
238
|
+
|
|
239
|
+
Aggregators/engines that have shipped models ahead of (or without) a correct
|
|
240
|
+
parser, producing the leakage the colleague observed:
|
|
241
|
+
|
|
242
|
+
- **Novita, Fireworks, OpenCode Zen, Tensorix** β reported by colleague for
|
|
243
|
+
DeepSeek V4-Flash.
|
|
244
|
+
- **NVIDIA** integrate endpoint β DSML leaked as assistant text (DeepSeek V4).
|
|
245
|
+
- **OpenRouter** β Kimi K2.6 inline tokens (sparse toolsets), Claude raw tags.
|
|
246
|
+
- **Microsoft Foundry / CherryIN** β DeepSeek V4 DSML.
|
|
247
|
+
- Self-hosted **vLLM / SGLang / MLX / llama.cpp / Ollama** across all families
|
|
248
|
+
(parser version mismatches, batched-decode special-token stripping).
|
|
249
|
+
|
|
250
|
+
## Design implications for the opt-in flag
|
|
251
|
+
|
|
252
|
+
1. **Disabled by default**, configured at
|
|
253
|
+
`~/.pi/agent/extensions/pi-tool-repair.json`. Likely shape:
|
|
254
|
+
```json
|
|
255
|
+
{
|
|
256
|
+
"grammarRepair": {
|
|
257
|
+
"enabled": true,
|
|
258
|
+
"grammars": ["dsml", "minimax", "qwen", "kimi", "mistral", "llama", "glm", "granite", "olmo"],
|
|
259
|
+
"mode": "recover" // "recover" = promote to tool_calls; "strip" = remove from visible text only
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
Default to all-known grammars when `enabled` and `grammars` omitted; allow
|
|
264
|
+
per-grammar opt-in/out.
|
|
265
|
+
|
|
266
|
+
2. **Recover, don't just strip.** Stripping alone leaves the agent loop with
|
|
267
|
+
`finish_reason: stop` and no tool dispatched (silent no-op). The valuable
|
|
268
|
+
behavior is parsing the leaked grammar into structured tool calls and
|
|
269
|
+
rewriting `finish_reason` β `tool_calls` (the path Cherry Studio, OpenClaw,
|
|
270
|
+
and Hermes all converged on). Fall back to strip when the block is malformed.
|
|
271
|
+
|
|
272
|
+
3. **Only recover complete, well-formed blocks with a known tool name and
|
|
273
|
+
parseable args.** Leave incomplete/malformed markup as text (or surface a
|
|
274
|
+
parse error) β do not promote arbitrary prose into execution.
|
|
275
|
+
|
|
276
|
+
4. **Stream-aware buffering.** Markers split across chunks; buffer a bounded
|
|
277
|
+
prefix (marker-length window) before emitting visible text, with a cap
|
|
278
|
+
(Kimi parser uses ~1KB buffer / 8KB section cap) and a final flush.
|
|
279
|
+
|
|
280
|
+
5. **Respect DSML `string="true|false"`** when building arguments; unwrap nested
|
|
281
|
+
`{"arguments": "{...}"}` wrappers; normalize Kimi tool-call IDs to
|
|
282
|
+
`functions.{name}:{idx}`.
|
|
283
|
+
|
|
284
|
+
6. **Code-fence / prose guard.** Don't strip grammar that appears inside fenced
|
|
285
|
+
code blocks or when the user/assistant is legitimately discussing the syntax
|
|
286
|
+
(the bug OpenClaw hit with `stripToolCallXmlTags`).
|
|
287
|
+
|
|
288
|
+
7. **Match the colleague's exact three DSML variants first** (double-bar,
|
|
289
|
+
single-bar, no-lead-bar), then generalize to the table above.
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-tool-repair",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Validate-then-repair extension for pi β fixes common LLM tool-call mistakes (null fields, stringified arrays, wrong field names, anchor bleed) before tools execute",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": "Tom X Nguyen",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/monotykamary/pi-tool-repair.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/monotykamary/pi-tool-repair#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/monotykamary/pi-tool-repair/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"pi-package",
|
|
18
|
+
"pi",
|
|
19
|
+
"pi-coding-agent",
|
|
20
|
+
"extension",
|
|
21
|
+
"tool-repair",
|
|
22
|
+
"open-models",
|
|
23
|
+
"kimi",
|
|
24
|
+
"deepseek",
|
|
25
|
+
"validate-then-repair",
|
|
26
|
+
"anchor-bleed",
|
|
27
|
+
"grammar-repair",
|
|
28
|
+
"dsml"
|
|
29
|
+
],
|
|
30
|
+
"files": [
|
|
31
|
+
"*.ts",
|
|
32
|
+
"src/",
|
|
33
|
+
"docs/",
|
|
34
|
+
"README.md"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"test:watch": "vitest",
|
|
39
|
+
"test:coverage": "vitest run --coverage",
|
|
40
|
+
"typecheck": "tsc --noEmit",
|
|
41
|
+
"lint:dead": "knip --no-gitignore"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@earendil-works/pi-coding-agent": "0.75.4",
|
|
45
|
+
"@types/node": "25.9.1",
|
|
46
|
+
"@vitest/coverage-v8": "4.1.7",
|
|
47
|
+
"knip": "6.14.1",
|
|
48
|
+
"typescript": "6.0.3",
|
|
49
|
+
"vitest": "4.1.7"
|
|
50
|
+
},
|
|
51
|
+
"pi": {
|
|
52
|
+
"extensions": [
|
|
53
|
+
"./tool-repair.ts"
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
}
|