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 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
+ [![pi extension](https://img.shields.io/badge/pi-extension-blueviolet)](https://github.com/earendil-works/pi-coding-agent)
10
+ [![license](https://img.shields.io/badge/license-MIT-blue)](./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
+ }