@madgagarin/pi-agentrouter 1.2.0 → 1.3.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 +3 -0
- package/index.ts +107 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -120,6 +120,9 @@ AgentRouter performs client fingerprint verification. Raw summarization requests
|
|
|
120
120
|
#### Q: Does the 2.5s pacing delay affect local or other cloud models?
|
|
121
121
|
No. The pacing logic specifically filters for AgentRouter endpoints (`isAgentRouter`). Native OpenAI, Anthropic, Gemini, or local models run at full speed without delay.
|
|
122
122
|
|
|
123
|
+
#### Q: How to use custom subagents with AgentRouter?
|
|
124
|
+
AgentRouter strictly verifies client authenticity (`pi-code` / `claude-code` prompt signature). If you define custom subagents in extensions like `pi-subagents`, make sure to specify `systemPromptMode: append` in your agent definition frontmatter so the base Pi system prompt identity is preserved.
|
|
125
|
+
|
|
123
126
|
---
|
|
124
127
|
|
|
125
128
|
## 📄 License
|
package/index.ts
CHANGED
|
@@ -90,7 +90,27 @@ export function fixPackagePriorityInSettings(): boolean {
|
|
|
90
90
|
const initialConfig = loadConfig();
|
|
91
91
|
let currentApiKey = normalizeApiKey(process.env.AGENTROUTER_API_KEY || initialConfig.apiKey || "");
|
|
92
92
|
let minIntervalMs = initialConfig.minIntervalMs ?? 2500;
|
|
93
|
-
|
|
93
|
+
const PACING_FILE = path.join(process.env.HOME || "", ".pi/agent/.agentrouter-pacing");
|
|
94
|
+
|
|
95
|
+
export function getLastRequestEndTime(): number {
|
|
96
|
+
try {
|
|
97
|
+
if (fs.existsSync(PACING_FILE)) {
|
|
98
|
+
const val = parseInt(fs.readFileSync(PACING_FILE, "utf-8").trim(), 10);
|
|
99
|
+
if (!isNaN(val)) return val;
|
|
100
|
+
}
|
|
101
|
+
} catch {}
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function setLastRequestEndTime(ts: number): void {
|
|
106
|
+
try {
|
|
107
|
+
const dir = path.dirname(PACING_FILE);
|
|
108
|
+
if (!fs.existsSync(dir)) {
|
|
109
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
110
|
+
}
|
|
111
|
+
fs.writeFileSync(PACING_FILE, String(ts), "utf-8");
|
|
112
|
+
} catch {}
|
|
113
|
+
}
|
|
94
114
|
|
|
95
115
|
export function isAgentRouter(providerName?: string, baseUrl?: string): boolean {
|
|
96
116
|
if (providerName && providerName.toLowerCase().includes("agentrouter")) return true;
|
|
@@ -98,6 +118,53 @@ export function isAgentRouter(providerName?: string, baseUrl?: string): boolean
|
|
|
98
118
|
return false;
|
|
99
119
|
}
|
|
100
120
|
|
|
121
|
+
export const CANONICAL_PI_HEADER =
|
|
122
|
+
"You are an expert coding assistant operating inside pi, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.";
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Enforces that the canonical pi-code system prompt signature is strictly at index 0.
|
|
126
|
+
* If another plugin or wrapper prepended text before the canonical header, it reorders
|
|
127
|
+
* the header to the very top and shifts the injected prefix right after it.
|
|
128
|
+
* If the header is missing entirely (e.g. from replace mode), it prepends the canonical header.
|
|
129
|
+
* This guarantees both WAF client authentication and stable prompt cache prefix matching.
|
|
130
|
+
*/
|
|
131
|
+
export function enforceCanonicalRootPrompt(systemPrompt: string | any[] | undefined): string | any[] {
|
|
132
|
+
if (!systemPrompt) {
|
|
133
|
+
return CANONICAL_PI_HEADER;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (typeof systemPrompt === "string") {
|
|
137
|
+
const text = systemPrompt.trim();
|
|
138
|
+
const piHeaderRegex = /(?:You are [^\n\r]*operating inside pi[^\n\r]*\n?|You are (?:pi|Pi)[^\n\r]*\n?)/i;
|
|
139
|
+
|
|
140
|
+
if (piHeaderRegex.test(text)) {
|
|
141
|
+
const match = text.match(piHeaderRegex);
|
|
142
|
+
if (match && match.index !== undefined && match.index > 0) {
|
|
143
|
+
const header = match[0].trim();
|
|
144
|
+
const prefix = text.slice(0, match.index).trim();
|
|
145
|
+
const rest = text.slice(match.index + match[0].length).trim();
|
|
146
|
+
return `${header}\n\n${prefix}${rest ? "\n\n" + rest : ""}`;
|
|
147
|
+
}
|
|
148
|
+
return text;
|
|
149
|
+
} else {
|
|
150
|
+
return `${CANONICAL_PI_HEADER}\n\n${text}`;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (Array.isArray(systemPrompt)) {
|
|
155
|
+
if (systemPrompt.length === 0) {
|
|
156
|
+
return [{ type: "text", text: CANONICAL_PI_HEADER }];
|
|
157
|
+
}
|
|
158
|
+
const firstBlock = systemPrompt[0];
|
|
159
|
+
if (firstBlock && typeof firstBlock.text === "string") {
|
|
160
|
+
firstBlock.text = enforceCanonicalRootPrompt(firstBlock.text) as string;
|
|
161
|
+
}
|
|
162
|
+
return systemPrompt;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return systemPrompt;
|
|
166
|
+
}
|
|
167
|
+
|
|
101
168
|
export default function (pi: ExtensionAPI) {
|
|
102
169
|
function registerAgentRouterProviders(apiKey: string): void {
|
|
103
170
|
pi.registerProvider("agentrouter-openai", {
|
|
@@ -129,8 +196,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
129
196
|
baseUrl: "https://agentrouter.org",
|
|
130
197
|
apiKey,
|
|
131
198
|
api: "anthropic-messages",
|
|
199
|
+
|
|
132
200
|
compat: {
|
|
133
201
|
forceAdaptiveThinking: true,
|
|
202
|
+
sendSessionAffinityHeaders: true,
|
|
134
203
|
},
|
|
135
204
|
models: [
|
|
136
205
|
{
|
|
@@ -143,6 +212,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
143
212
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
144
213
|
compat: {
|
|
145
214
|
forceAdaptiveThinking: true,
|
|
215
|
+
sendSessionAffinityHeaders: true,
|
|
146
216
|
},
|
|
147
217
|
},
|
|
148
218
|
{
|
|
@@ -155,6 +225,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
155
225
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
156
226
|
compat: {
|
|
157
227
|
forceAdaptiveThinking: true,
|
|
228
|
+
sendSessionAffinityHeaders: true,
|
|
158
229
|
},
|
|
159
230
|
},
|
|
160
231
|
],
|
|
@@ -171,9 +242,37 @@ export default function (pi: ExtensionAPI) {
|
|
|
171
242
|
}
|
|
172
243
|
}
|
|
173
244
|
|
|
245
|
+
// Enforce root system prompt signature and prompt prefix stability at the lowest transport level
|
|
246
|
+
pi.on("before_provider_request", async (event, ctx) => {
|
|
247
|
+
const provider = ((event as any)?.model?.provider || ctx?.model?.provider || "").toLowerCase();
|
|
248
|
+
const baseUrl = ((event as any)?.model?.baseUrl || (ctx?.model as any)?.baseUrl || "");
|
|
249
|
+
|
|
250
|
+
if (isAgentRouter(provider, baseUrl)) {
|
|
251
|
+
const payload = event.payload;
|
|
252
|
+
if (payload) {
|
|
253
|
+
// 1. Enforce canonical root system prompt in payload.system (Anthropic format)
|
|
254
|
+
if (payload.system !== undefined) {
|
|
255
|
+
payload.system = enforceCanonicalRootPrompt(payload.system);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// 2. Enforce canonical root system prompt in payload.messages[0] (OpenAI format)
|
|
259
|
+
if (Array.isArray(payload.messages) && payload.messages.length > 0) {
|
|
260
|
+
const firstMsg = payload.messages[0];
|
|
261
|
+
if (firstMsg && firstMsg.role === "system") {
|
|
262
|
+
if (typeof firstMsg.content === "string") {
|
|
263
|
+
firstMsg.content = enforceCanonicalRootPrompt(firstMsg.content);
|
|
264
|
+
} else if (Array.isArray(firstMsg.content)) {
|
|
265
|
+
firstMsg.content = enforceCanonicalRootPrompt(firstMsg.content);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
174
273
|
pi.on("session_start", async (_event, ctx) => {
|
|
175
274
|
updatePromptRewriteEnvForModel(ctx.model);
|
|
176
|
-
|
|
275
|
+
setLastRequestEndTime(Date.now());
|
|
177
276
|
|
|
178
277
|
const order = getPackageOrderState();
|
|
179
278
|
if (order.needsFix && ctx.hasUI && typeof (ctx.ui as any).confirm === "function") {
|
|
@@ -209,10 +308,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
209
308
|
return;
|
|
210
309
|
}
|
|
211
310
|
|
|
311
|
+
const lastEnd = getLastRequestEndTime();
|
|
212
312
|
const now = Date.now();
|
|
213
|
-
const elapsed = now -
|
|
313
|
+
const elapsed = now - lastEnd;
|
|
214
314
|
|
|
215
|
-
if (
|
|
315
|
+
if (lastEnd > 0 && elapsed < minIntervalMs) {
|
|
216
316
|
const waitMs = minIntervalMs - elapsed;
|
|
217
317
|
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
218
318
|
}
|
|
@@ -221,14 +321,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
221
321
|
pi.on("turn_end", async (_event, ctx) => {
|
|
222
322
|
const model = ctx.model;
|
|
223
323
|
if (isAgentRouter(model?.provider, (model as any)?.baseUrl)) {
|
|
224
|
-
|
|
324
|
+
setLastRequestEndTime(Date.now());
|
|
225
325
|
}
|
|
226
326
|
});
|
|
227
327
|
|
|
228
328
|
pi.on("agent_end", async (_event, ctx) => {
|
|
229
329
|
const model = ctx.model;
|
|
230
330
|
if (isAgentRouter(model?.provider, (model as any)?.baseUrl)) {
|
|
231
|
-
|
|
331
|
+
setLastRequestEndTime(Date.now());
|
|
232
332
|
}
|
|
233
333
|
});
|
|
234
334
|
|
|
@@ -276,6 +376,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
276
376
|
signal,
|
|
277
377
|
}
|
|
278
378
|
);
|
|
379
|
+
setLastRequestEndTime(Date.now());
|
|
279
380
|
|
|
280
381
|
const summary = response.content
|
|
281
382
|
.filter((c: any): c is { type: "text"; text: string } => c.type === "text")
|
package/package.json
CHANGED