pi-tool-repair 0.1.12 → 0.2.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 CHANGED
@@ -145,6 +145,24 @@ Modes:
145
145
  | `recover` | Strip leaked markup and append recovered pi `toolCall` blocks. |
146
146
  | `strip` | Strip leaked markup only; do not execute recovered calls. |
147
147
 
148
+ #### Per-model enablement
149
+
150
+ If only some of your models leak grammar — common with local servers such as llama.cpp, vLLM, or Ollama — auto-enable recovery per model id with `leakModels`. Entries are case-insensitive regex fragments matched against the active model id:
151
+
152
+ ```json
153
+ {
154
+ "grammarRepair": {
155
+ "leakModels": ["kimi", "qwen3", "gguf"]
156
+ }
157
+ }
158
+ ```
159
+
160
+ Recovery turns on whenever the session's model id matches a pattern. Global `enabled: true` takes precedence over `leakModels`, so models with reliable native tool calling stay untouched. Regex entries that fail to compile are ignored.
161
+
162
+ #### What the model sees on the next request
163
+
164
+ Every repair runs on pi's `message_end` hook, where the repaired message is replaced in place — the corrected call, not the model's original output, is what pi writes to the session file and resends on later requests. With `mode: "recover"`, leaked tool-call text is likewise converted into real `toolCall` blocks before persistence, so subsequent requests show the model a properly formed call plus its tool results: an in-context correction loop instead of a silent execute-time patch. Local models benefit the most since there is no prompt-cache penalty for the rewritten history; providers that cache by prefix may treat the first turn after a repair as a cache miss.
165
+
148
166
  Safety gates:
149
167
 
150
168
  - `requireKnownTool: true` only recovers calls whose name is in pi's active tool registry.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-tool-repair",
3
- "version": "0.1.12",
3
+ "version": "0.2.0",
4
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
5
  "type": "module",
6
6
  "author": "Tom X Nguyen",
@@ -24,6 +24,9 @@ export interface GrammarRepairConfig {
24
24
  mode: GrammarRepairMode;
25
25
  requireKnownTool: boolean;
26
26
  debug: boolean;
27
+ // Case-insensitive regex fragments that auto-enable grammar repair when the
28
+ // active model id matches one of them. Ignored while `enabled` is true.
29
+ leakModels?: string[];
27
30
  }
28
31
 
29
32
  export interface ExtensionFileConfig {
@@ -119,15 +122,52 @@ export function normalizeGrammarRepairConfig(raw: Partial<GrammarRepairConfig> =
119
122
  ? raw.grammars.filter((name): name is GrammarName => grammarSet.has(name as GrammarName))
120
123
  : ALL_GRAMMARS;
121
124
 
125
+ const leakModels = normalizeLeakModels(raw.leakModels);
126
+
122
127
  return {
123
128
  enabled: raw.enabled ?? DEFAULT_GRAMMAR_REPAIR_CONFIG.enabled,
124
129
  grammars: grammars.length > 0 ? grammars : ALL_GRAMMARS,
125
130
  mode: raw.mode === "strip" ? "strip" : "recover",
126
131
  requireKnownTool: raw.requireKnownTool ?? DEFAULT_GRAMMAR_REPAIR_CONFIG.requireKnownTool,
127
132
  debug: raw.debug ?? DEFAULT_GRAMMAR_REPAIR_CONFIG.debug,
133
+ ...(leakModels ? { leakModels } : {}),
128
134
  };
129
135
  }
130
136
 
137
+ const compileLeakModelPattern = (pattern: string): RegExp | undefined => {
138
+ try {
139
+ return new RegExp(pattern, "i");
140
+ } catch {
141
+ return undefined;
142
+ }
143
+ };
144
+
145
+ function normalizeLeakModels(raw: unknown): string[] | undefined {
146
+ if (!Array.isArray(raw)) return undefined;
147
+ const valid = raw.filter(
148
+ (pattern): pattern is string =>
149
+ typeof pattern === "string" && compileLeakModelPattern(pattern) !== undefined,
150
+ );
151
+ return valid.length > 0 ? valid : undefined;
152
+ }
153
+
154
+ // Enables grammar repair for the current message when the active model id
155
+ // matches a configured leakModels pattern. Global `enabled: true` always wins,
156
+ // so models with native tool calling keep the recovery path off unless listed.
157
+ export function resolveGrammarRepairForModel(
158
+ config: GrammarRepairConfig,
159
+ model: { id?: string } | null | undefined,
160
+ ): GrammarRepairConfig {
161
+ if (config.enabled) return config;
162
+ const patterns = config.leakModels;
163
+ if (!patterns || patterns.length === 0) return config;
164
+ const modelId = model?.id;
165
+ if (typeof modelId !== "string" || modelId.length === 0) return config;
166
+ return patterns.some((pattern) => compileLeakModelPattern(pattern)?.test(modelId))
167
+ ? { ...config, enabled: true }
168
+ : config;
169
+ }
170
+
131
171
  export function repairAssistantMessageGrammarLeaks(
132
172
  message: MinimalAssistantMessage,
133
173
  config: GrammarRepairConfig,
package/tool-repair.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  logRepair,
18
18
  loadGrammarRepairConfig,
19
19
  repairAssistantMessageGrammarLeaks,
20
+ resolveGrammarRepairForModel,
20
21
  repairAssistantToolCallInputs,
21
22
  normalizePhantomToolUse,
22
23
  type MinimalAssistantMessage,
@@ -117,25 +118,26 @@ export default function (pi: ExtensionAPI) {
117
118
  if (model && hasAnchorBleedBug(model) && stripAnchorBleedInPlace(args)) changed = true;
118
119
  }
119
120
 
120
- if (grammarRepairConfig.enabled) {
121
+ const effectiveGrammarRepairConfig = resolveGrammarRepairForModel(grammarRepairConfig, model);
122
+ if (effectiveGrammarRepairConfig.enabled) {
121
123
  const knownTools = new Set(
122
124
  safeGetActiveTools(pi)
123
125
  .filter((name): name is string => typeof name === "string" && name.length > 0),
124
126
  );
125
127
  const grammarResult = repairAssistantMessageGrammarLeaks(
126
128
  message,
127
- grammarRepairConfig,
129
+ effectiveGrammarRepairConfig,
128
130
  knownTools,
129
131
  );
130
132
  if (grammarResult.changed) {
131
133
  message = grammarResult.message;
132
134
  changed = true;
133
- if (grammarRepairConfig.debug) {
135
+ if (effectiveGrammarRepairConfig.debug) {
134
136
  const calls = grammarResult.recoveredCalls
135
137
  .map((call) => `${call.grammar}:${call.name}`)
136
138
  .join(",") || "none";
137
139
  process.stderr.write(
138
- `[pi-tool-repair] grammar-repair mode=${grammarRepairConfig.mode} ` +
140
+ `[pi-tool-repair] grammar-repair mode=${effectiveGrammarRepairConfig.mode} ` +
139
141
  `stripped=${grammarResult.strippedRanges} recovered=${calls}\n`,
140
142
  );
141
143
  }