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
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
/**
* rename-pi — sanitize "pi" -> "the assistant" for the claude-proxy. (ToS violatooooor)
*
* WHY TWO HOOKS:
* The claude-proxy classifies requests by system-prompt content; bare "pi"
* references get billed as extra usage. Renaming must therefore reach EVERY
* outbound request.
*
* `before_agent_start` only fires when a fresh user message is sent, and the
* system prompt it returns is later CLOBBERED whenever the prompt is rebuilt
* mid-run (e.g. pi-goal's create_goal -> setActiveTools -> _rebuildSystemPrompt
* regenerates the raw prompt with "pi"/"pi-goal" and the "Pi documentation"
* section). Goal continuations then ship ~40 un-sanitized "pi" refs and get
* billed as extra usage. That was the bug.
*
* `before_provider_request` fires at the single choke point for ALL provider
* calls (typed turns, goal continuations, subagents, summarization), with the
* fully-built request payload, and can replace it. Sanitizing `payload.system`
* here is leak-proof by construction — no lifecycle rebuild can bypass it.
*
* We keep both: before_agent_start preserves the renamed prompt in-session/for
* display; before_provider_request guarantees the wire is always sanitized.
* The rewrite is idempotent ("the assistant" contains no bare "pi"), so running
* both never double-mangles.
*
* Safety: only touches `payload.system` (string or Anthropic text-block array),
* matching the original systemPrompt-only behavior. Other providers / shapes
* are left untouched. Handler is defensive and never throws (the runner also
* catches, but we belt-and-suspenders it).
*/
// Word-boundary-aware: only bare "pi"/"Pi" as standalone words. "the assistant"
// for grammatically correct output. Skips possessives / compounds mid-word.
function sanitize(text: string): string {
return text.replace(/\bpi\b/g, "the assistant").replace(/\bPi\b/g, "The assistant");
}
// Sanitize the outbound request payload's system prompt in place (Anthropic
// `system` is an array of {type:"text", text} blocks; some APIs use a string).
// Returns a count of blocks changed, for optional debug.
function sanitizePayloadSystem(payload: any): number {
let changed = 0;
if (!payload || typeof payload !== "object") return changed;
const sys = payload.system;
if (typeof sys === "string") {
const next = sanitize(sys);
if (next !== sys) {
payload.system = next;
changed++;
}
} else if (Array.isArray(sys)) {
for (const block of sys) {
if (block && typeof block.text === "string") {
const next = sanitize(block.text);
if (next !== block.text) {
block.text = next;
changed++;
}
}
}
}
return changed;
}
export default function (pi: ExtensionAPI) {
// (1) In-session / display parity: rename the prompt shown to the model when a
// fresh user turn starts. (Leaky on its own — see header — hence hook 2.)
pi.on("before_agent_start", async (event, _ctx) => {
return { systemPrompt: sanitize(event.systemPrompt) };
});
// (2) Wire guarantee: rewrite the system prompt on the actual outbound payload
// for every provider request, regardless of which code path built it.
pi.on("before_provider_request", async (event, _ctx) => {
const payload = (event as any).payload;
const changed = sanitizePayloadSystem(payload);
if (changed && process.env.PI_SANITIZE_DEBUG) {
try {
process.stderr.write(`[rename-pi] sanitized ${changed} system block(s) on provider request\n`);
} catch {
/* ignore */
}
}
return payload;
});
}
|