@madgagarin/pi-agentrouter 2.1.3 → 2.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 +5 -0
- package/index.ts +333 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,7 +37,9 @@ pi install npm:@madgagarin/pi-agentrouter
|
|
|
37
37
|
## Features
|
|
38
38
|
|
|
39
39
|
- **DeepSeek Native Multi-Turn Tool Calling:** Seamlessly preserves native `tool_calls` and guarantees non-empty `reasoning_content` across multi-turn execution, ensuring fully autonomous coding agent loops.
|
|
40
|
+
- **Compaction & Token Footprint Optimization:** Transparently intercepts `/compact` summarization requests at the transport level (`globalThis.fetch`), strips bulky thinking scratchpads and file dumps, compressing compaction payloads from ~1.65 MB down to ~200 KB.
|
|
40
41
|
- **WAF Bypass & Language Normalization:** Automatically replaces false-positive upstream WAF keywords (such as Russian `Ключевое` → `Главное`), cleans terminal ANSI sequences, and ensures persistent language adhering via technical preamble.
|
|
42
|
+
- **Gateway Auto-Retry & Fault Tolerance:** Automatic retry loop for transient upstream glitches (temporary 500, 503, or thinking mode channel hops) with diagnostic logging to `~/.pi/agent/.agentrouter-debug.log`.
|
|
41
43
|
- **Model Synchronization:** Automatically registers and adds active models to `enabledModels` in `settings.json` for quick selection via `Ctrl+P`.
|
|
42
44
|
- **Schema Sanitization:** Automatically normalizes tool definitions (e.g. converting `required: null` to empty arrays) for strict OpenAI schema validation compatibility.
|
|
43
45
|
- **WAF Diagnostics & Safe Redaction:** Intercepts upstream blocks and safely redacts older messages while preserving thinking placeholders for reasoning models.
|
|
@@ -124,6 +126,9 @@ No. Request pacing only applies when talking to `agentrouter.org` endpoints. Loc
|
|
|
124
126
|
#### Using custom subagents (`pi-subagents`)
|
|
125
127
|
AgentRouter requires the base `pi-code` prompt signature for authentication. If you create custom subagents in `~/.pi/agent/agents/*.md`, make sure their frontmatter uses `systemPromptMode: append`.
|
|
126
128
|
|
|
129
|
+
#### How does Gateway Resilience and Fault Tolerance work?
|
|
130
|
+
Upstream LLM gateways can occasionally encounter transient channel hops or temporary thinking-mode validation errors (`400: in the thinking mode must be passed back`, `500 temporarily unavailable`, `503`). The extension includes a transparent transport-level interceptor (`installAgentRouterFetchHook`) on `globalThis.fetch` that sanitizes reasoning parameters, normalizes headers, and automatically retries transient errors with exponential backoff so your coding sessions continue uninterrupted. Diagnostic events are logged to `~/.pi/agent/.agentrouter-debug.log`.
|
|
131
|
+
|
|
127
132
|
---
|
|
128
133
|
|
|
129
134
|
## License
|
package/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { convertToLlm, serializeConversation } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import * as path from "path";
|
|
5
|
+
import * as os from "os";
|
|
5
6
|
|
|
6
7
|
const CONFIG_FILE = path.join(process.env.HOME || "", ".pi/agent/agentrouter.json");
|
|
7
8
|
const SETTINGS_FILE = path.join(process.env.HOME || "", ".pi/agent/settings.json");
|
|
@@ -61,7 +62,6 @@ export const KNOWN_MODEL_SPECS: Record<string, ModelSpec> = {
|
|
|
61
62
|
compat: {
|
|
62
63
|
sendSessionAffinityHeaders: true,
|
|
63
64
|
requiresReasoningContentOnAssistantMessages: true,
|
|
64
|
-
thinkingFormat: "deepseek",
|
|
65
65
|
},
|
|
66
66
|
cost: { input: 4.0 / 1_000_000, output: 12.0 / 1_000_000, cacheRead: 2.0 / 1_000_000, cacheWrite: 0 },
|
|
67
67
|
},
|
|
@@ -75,7 +75,6 @@ export const KNOWN_MODEL_SPECS: Record<string, ModelSpec> = {
|
|
|
75
75
|
compat: {
|
|
76
76
|
sendSessionAffinityHeaders: true,
|
|
77
77
|
requiresReasoningContentOnAssistantMessages: true,
|
|
78
|
-
thinkingFormat: "deepseek",
|
|
79
78
|
},
|
|
80
79
|
cost: { input: 4.0 / 1_000_000, output: 12.0 / 1_000_000, cacheRead: 2.0 / 1_000_000, cacheWrite: 0 },
|
|
81
80
|
},
|
|
@@ -351,6 +350,14 @@ export const CANONICAL_PI_HEADER =
|
|
|
351
350
|
export const LANGUAGE_PREAMBLE =
|
|
352
351
|
"[Instruction: You are an expert coding assistant operating inside pi. Please carefully analyze the technical context, understand the user request, follow all project instructions and coding standards, and respond thoroughly in the requested language.]";
|
|
353
352
|
|
|
353
|
+
export function getPiUserAgent(): string {
|
|
354
|
+
try {
|
|
355
|
+
return `pi (${os.platform()} ${os.release()}; ${os.arch()})`;
|
|
356
|
+
} catch {
|
|
357
|
+
return "pi (browser)";
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
354
361
|
export function sanitizeDeepSeekText(text: string): string {
|
|
355
362
|
if (typeof text !== "string") return text;
|
|
356
363
|
// Replace false-positive blocked Russian word in AgentRouter upstream WAF
|
|
@@ -818,6 +825,171 @@ export function applyPoisonRedaction(payload: Record<string, unknown>): void {
|
|
|
818
825
|
}
|
|
819
826
|
}
|
|
820
827
|
|
|
828
|
+
export function isCompactionPayload(payload: Record<string, unknown>): boolean {
|
|
829
|
+
if (
|
|
830
|
+
typeof payload.system === "string" &&
|
|
831
|
+
(payload.system.includes("summarization assistant") || payload.system.includes("context summarization"))
|
|
832
|
+
) {
|
|
833
|
+
return true;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const messages = Array.isArray(payload.messages) ? payload.messages : (payload as any).input;
|
|
837
|
+
if (!Array.isArray(messages)) return false;
|
|
838
|
+
|
|
839
|
+
for (const m of messages) {
|
|
840
|
+
if (!m || typeof m !== "object") continue;
|
|
841
|
+
const role = (m as any).role;
|
|
842
|
+
const content = (m as any).content;
|
|
843
|
+
|
|
844
|
+
if (role === "system" || role === "developer") {
|
|
845
|
+
const text =
|
|
846
|
+
typeof content === "string"
|
|
847
|
+
? content
|
|
848
|
+
: Array.isArray(content)
|
|
849
|
+
? content.map((b: any) => b?.text || "").join(" ")
|
|
850
|
+
: "";
|
|
851
|
+
if (text.includes("summarization assistant") || text.includes("context summarization")) {
|
|
852
|
+
return true;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
if (typeof content === "string") {
|
|
857
|
+
if (
|
|
858
|
+
(content.includes("<conversation>") && content.includes("</conversation>")) ||
|
|
859
|
+
(content.includes("# Conversation") && content.includes("# Instructions")) ||
|
|
860
|
+
content.includes("The messages above are a conversation to summarize") ||
|
|
861
|
+
content.includes("This is the PREFIX of a turn that was too large to keep")
|
|
862
|
+
) {
|
|
863
|
+
return true;
|
|
864
|
+
}
|
|
865
|
+
} else if (Array.isArray(content)) {
|
|
866
|
+
for (const b of content) {
|
|
867
|
+
if (b && typeof b === "object" && typeof b.text === "string") {
|
|
868
|
+
if (
|
|
869
|
+
(b.text.includes("<conversation>") && b.text.includes("</conversation>")) ||
|
|
870
|
+
(b.text.includes("# Conversation") && b.text.includes("# Instructions")) ||
|
|
871
|
+
b.text.includes("The messages above are a conversation to summarize") ||
|
|
872
|
+
b.text.includes("This is the PREFIX of a turn that was too large to keep")
|
|
873
|
+
) {
|
|
874
|
+
return true;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
return false;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
export function cleanSerializedConversation(inner: string): string {
|
|
885
|
+
// Split on block boundaries: [User]:, [Assistant thinking]:, [Assistant]:, [Assistant tool calls]:, [Tool result]:
|
|
886
|
+
const blockRegex = /(?:^|\n+)(?=\[(?:User|Assistant thinking|Assistant|Assistant tool calls|Tool result)\]:)/;
|
|
887
|
+
const blocks = inner.split(blockRegex);
|
|
888
|
+
const out: string[] = [];
|
|
889
|
+
|
|
890
|
+
for (const rawBlock of blocks) {
|
|
891
|
+
const block = rawBlock.trim();
|
|
892
|
+
if (!block) continue;
|
|
893
|
+
|
|
894
|
+
// 1. Drop thinking blocks completely — thinking is scratchpad, wastes tokens, and contains sensitive reasoning tokens
|
|
895
|
+
if (block.startsWith("[Assistant thinking]:")) {
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// 2. Neutralize tool results — replace raw dumped files, scraped web pages, tokens, and diffs with clean placeholder
|
|
900
|
+
if (block.startsWith("[Tool result]:")) {
|
|
901
|
+
out.push("[Tool result]: [Content withheld by local policy]");
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// 3. Neutralize tool call arguments — keep tool name and signature clean without massive payload dumps
|
|
906
|
+
if (block.startsWith("[Assistant tool calls]:")) {
|
|
907
|
+
const stripped = block.replace(/\([\s\S]*?\)(?=;\s*|$)/g, "()");
|
|
908
|
+
out.push(stripped);
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// 4. Drop assistant messages that were WAF error dumps
|
|
913
|
+
if (block.startsWith("[Assistant]:")) {
|
|
914
|
+
if (WAF_BLOCK_RE.test(block)) {
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
out.push(block);
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
out.push(block);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
let res = out.join("\n\n");
|
|
925
|
+
res = sanitizeDeepSeekText(res);
|
|
926
|
+
return res;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
export function sanitizeCompactionText(text: string): string {
|
|
930
|
+
// Format 1: <conversation>...</conversation> (Standard compaction & branch summary)
|
|
931
|
+
if (text.includes("<conversation>")) {
|
|
932
|
+
text = text.replace(/<conversation>([\s\S]*?)<\/conversation>/g, (_m, inner) => {
|
|
933
|
+
return `<conversation>\n${cleanSerializedConversation(inner)}\n</conversation>`;
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Format 2: # Conversation ... # Instructions (Split-turn prefix summary)
|
|
938
|
+
if (text.includes("# Conversation")) {
|
|
939
|
+
text = text.replace(/# Conversation([\s\S]*?)(?=# Instructions|$)/g, (_m, inner) => {
|
|
940
|
+
return `# Conversation\n${cleanSerializedConversation(inner)}\n\n`;
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Format 3: Clean <previous-summary> if present
|
|
945
|
+
if (text.includes("<previous-summary>")) {
|
|
946
|
+
text = text.replace(/<previous-summary>([\s\S]*?)<\/previous-summary>/g, (_m, inner) => {
|
|
947
|
+
let cleaned = inner.replace(/Error: 500: \{"message":"sensitive words detected[\s\S]*?}/gi, "");
|
|
948
|
+
cleaned = cleaned.replace(/sensitive[_ ]words?[_ ]detected|content-blocked/gi, "");
|
|
949
|
+
cleaned = sanitizeDeepSeekText(cleaned);
|
|
950
|
+
return `<previous-summary>${cleaned}</previous-summary>`;
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
return text;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
export function sanitizeCompactionPayload(payload: Record<string, unknown>): boolean {
|
|
958
|
+
const messages = Array.isArray(payload.messages) ? payload.messages : (payload as any).input;
|
|
959
|
+
if (!Array.isArray(messages) || messages.length === 0) return false;
|
|
960
|
+
|
|
961
|
+
let sanitized = false;
|
|
962
|
+
for (const msg of messages) {
|
|
963
|
+
if (!msg || typeof msg !== "object") continue;
|
|
964
|
+
|
|
965
|
+
if (typeof msg.content === "string") {
|
|
966
|
+
if (
|
|
967
|
+
msg.content.includes("<conversation>") ||
|
|
968
|
+
msg.content.includes("# Conversation") ||
|
|
969
|
+
msg.content.includes("<previous-summary>")
|
|
970
|
+
) {
|
|
971
|
+
msg.content = sanitizeCompactionText(msg.content);
|
|
972
|
+
sanitized = true;
|
|
973
|
+
}
|
|
974
|
+
} else if (Array.isArray(msg.content)) {
|
|
975
|
+
for (const block of msg.content) {
|
|
976
|
+
if (block && typeof block === "object" && typeof block.text === "string") {
|
|
977
|
+
if (
|
|
978
|
+
block.text.includes("<conversation>") ||
|
|
979
|
+
block.text.includes("# Conversation") ||
|
|
980
|
+
block.text.includes("<previous-summary>")
|
|
981
|
+
) {
|
|
982
|
+
block.text = sanitizeCompactionText(block.text);
|
|
983
|
+
sanitized = true;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
return sanitized;
|
|
991
|
+
}
|
|
992
|
+
|
|
821
993
|
export function normalizeMessagesForAgentRouter(messages: any[], isDeepSeek: boolean = false): void {
|
|
822
994
|
if (!Array.isArray(messages)) return;
|
|
823
995
|
|
|
@@ -916,9 +1088,6 @@ export function normalizeMessagesForAgentRouter(messages: any[], isDeepSeek: boo
|
|
|
916
1088
|
}
|
|
917
1089
|
|
|
918
1090
|
if (isDeepSeek) {
|
|
919
|
-
// Flatten past assistant tool_calls into text and convert tool roles into user turns.
|
|
920
|
-
// This prevents AgentRouter's upstream Anthropic gateway from rejecting the request with:
|
|
921
|
-
// "400: The `content[].thinking` in the thinking mode must be passed back to the API."
|
|
922
1091
|
// Ensure assistant tool calls retain non-empty reasoning_content for gateway compatibility
|
|
923
1092
|
if (
|
|
924
1093
|
msg.role === "assistant" &&
|
|
@@ -1075,7 +1244,7 @@ export function checkAndNotifyContentBlocked(errMessage: string | undefined, ctx
|
|
|
1075
1244
|
export async function fetchLivePricing(): Promise<ApiPricingModel[] | null> {
|
|
1076
1245
|
try {
|
|
1077
1246
|
const res = await fetch("https://agentrouter.org/api/pricing", {
|
|
1078
|
-
|
|
1247
|
+
headers: { "User-Agent": getPiUserAgent() },
|
|
1079
1248
|
});
|
|
1080
1249
|
if (!res.ok) return null;
|
|
1081
1250
|
const data = await res.json();
|
|
@@ -1099,7 +1268,8 @@ export async function fetchTokenUsage(apiKey: string): Promise<number | null> {
|
|
|
1099
1268
|
{
|
|
1100
1269
|
headers: {
|
|
1101
1270
|
Authorization: `Bearer ${apiKey}`,
|
|
1102
|
-
|
|
1271
|
+
"User-Agent": getPiUserAgent(),
|
|
1272
|
+
},
|
|
1103
1273
|
}
|
|
1104
1274
|
);
|
|
1105
1275
|
if (!res.ok) return null;
|
|
@@ -1177,7 +1347,134 @@ export async function probeModelQuota(modelId: string, apiKey: string, isAnthrop
|
|
|
1177
1347
|
}
|
|
1178
1348
|
}
|
|
1179
1349
|
|
|
1350
|
+
function arDebugLog(msg: string): void {
|
|
1351
|
+
try {
|
|
1352
|
+
const logFile = path.join(process.env.HOME || "", ".pi/agent/.agentrouter-debug.log");
|
|
1353
|
+
fs.appendFileSync(logFile, `[${new Date().toISOString()}] ${msg}\n`, "utf-8");
|
|
1354
|
+
} catch {}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
let fetchHookInstalled = false;
|
|
1358
|
+
|
|
1359
|
+
export function installAgentRouterFetchHook(): void {
|
|
1360
|
+
if (fetchHookInstalled) return;
|
|
1361
|
+
fetchHookInstalled = true;
|
|
1362
|
+
arDebugLog("installAgentRouterFetchHook successfully registered on globalThis.fetch");
|
|
1363
|
+
|
|
1364
|
+
const originalFetch = globalThis.fetch;
|
|
1365
|
+
globalThis.fetch = async function (input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
|
|
1366
|
+
const urlStr = typeof input === "string" ? input : input instanceof URL ? input.toString() : (input as Request)?.url || "";
|
|
1367
|
+
|
|
1368
|
+
if (urlStr.includes("agentrouter.org")) {
|
|
1369
|
+
if (init?.headers) {
|
|
1370
|
+
if (init.headers instanceof Headers) {
|
|
1371
|
+
if (init.headers.get("User-Agent") === "pi-code") {
|
|
1372
|
+
init.headers.set("User-Agent", getPiUserAgent());
|
|
1373
|
+
}
|
|
1374
|
+
} else if (typeof init.headers === "object") {
|
|
1375
|
+
for (const [k, v] of Object.entries(init.headers)) {
|
|
1376
|
+
if (k.toLowerCase() === "user-agent" && v === "pi-code") {
|
|
1377
|
+
(init.headers as any)[k] = getPiUserAgent();
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
if (init && typeof init.body === "string") {
|
|
1384
|
+
try {
|
|
1385
|
+
const body = JSON.parse(init.body);
|
|
1386
|
+
if (body && typeof body === "object") {
|
|
1387
|
+
const modelId = typeof body.model === "string" ? body.model.toLowerCase() : "";
|
|
1388
|
+
const isDeepSeek = modelId.includes("deepseek");
|
|
1389
|
+
const isCompaction = isCompactionPayload(body);
|
|
1390
|
+
const origLen = init.body.length;
|
|
1391
|
+
|
|
1392
|
+
if (isDeepSeek && "thinking" in body) {
|
|
1393
|
+
delete body.thinking;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
if (isCompaction) {
|
|
1397
|
+
sanitizeCompactionPayload(body);
|
|
1398
|
+
const messages = Array.isArray(body.messages) ? body.messages : body.input;
|
|
1399
|
+
if (Array.isArray(messages) && messages.length > 0) {
|
|
1400
|
+
normalizeMessagesForAgentRouter(messages, isDeepSeek);
|
|
1401
|
+
if (isDeepSeek) {
|
|
1402
|
+
frameUserTurnsForDeepSeek(messages);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
init.body = JSON.stringify(body);
|
|
1406
|
+
arDebugLog(`[FetchHook] Compaction intercepted: model=${modelId} origLen=${origLen} newLen=${init.body.length}`);
|
|
1407
|
+
} else {
|
|
1408
|
+
applyPoisonRedaction(body);
|
|
1409
|
+
const messages = Array.isArray(body.messages) ? body.messages : body.input;
|
|
1410
|
+
if (Array.isArray(messages) && messages.length > 0) {
|
|
1411
|
+
normalizeMessagesForAgentRouter(messages, isDeepSeek);
|
|
1412
|
+
if (isDeepSeek) {
|
|
1413
|
+
frameUserTurnsForDeepSeek(messages);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
init.body = JSON.stringify(body);
|
|
1417
|
+
arDebugLog(`[FetchHook] Chat turn intercepted: model=${modelId} origLen=${origLen} newLen=${init.body.length}`);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
} catch (err: any) {
|
|
1421
|
+
arDebugLog(`[FetchHook] Error parsing body: ${err.message}`);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
let response = await originalFetch.call(this, input, init);
|
|
1427
|
+
|
|
1428
|
+
// If upstream returns a retryable error on AgentRouter (such as thinking mode glitch or temporary unavailability), retry up to 2 times
|
|
1429
|
+
if (urlStr.includes("agentrouter.org")) {
|
|
1430
|
+
for (let attempt = 0; attempt < 2 && !response.ok; attempt++) {
|
|
1431
|
+
const cloned = response.clone();
|
|
1432
|
+
const text = await cloned.text();
|
|
1433
|
+
const shouldRetry =
|
|
1434
|
+
(response.status === 400 && text.includes("in the thinking mode must be passed back")) ||
|
|
1435
|
+
(response.status === 500 && (text.includes("temporarily unavailable") || text.includes("sensitive words detected"))) ||
|
|
1436
|
+
response.status === 503;
|
|
1437
|
+
|
|
1438
|
+
if (!shouldRetry) break;
|
|
1439
|
+
|
|
1440
|
+
arDebugLog(`[FetchHook] Caught retryable upstream ${response.status}: ${text.slice(0, 80)}. Retrying attempt ${attempt + 1}...`);
|
|
1441
|
+
if (response.status === 400 && init && typeof init.body === "string") {
|
|
1442
|
+
try {
|
|
1443
|
+
const bodyObj = JSON.parse(init.body);
|
|
1444
|
+
if (bodyObj && typeof bodyObj === "object") {
|
|
1445
|
+
delete bodyObj.thinking;
|
|
1446
|
+
init.body = JSON.stringify(bodyObj);
|
|
1447
|
+
}
|
|
1448
|
+
} catch {}
|
|
1449
|
+
}
|
|
1450
|
+
await new Promise((resolve) => setTimeout(resolve, (attempt + 1) * 500));
|
|
1451
|
+
response = await originalFetch.call(this, input, init);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// Log and handle non-OK responses from upstream
|
|
1456
|
+
if (urlStr.includes("agentrouter.org") && !response.ok) {
|
|
1457
|
+
try {
|
|
1458
|
+
const cloned = response.clone();
|
|
1459
|
+
const text = await cloned.text();
|
|
1460
|
+
arDebugLog(`[FetchHook] Upstream ${response.status}: ${text.slice(0, 200)}`);
|
|
1461
|
+
if (WAF_BLOCK_RE.test(text)) {
|
|
1462
|
+
escalatePending = true;
|
|
1463
|
+
if (SENSITIVE_WORDS_RE.test(text)) {
|
|
1464
|
+
isSensitiveBlock = true;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
} catch {}
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
return response;
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
installAgentRouterFetchHook();
|
|
1475
|
+
|
|
1180
1476
|
export default function (pi: ExtensionAPI) {
|
|
1477
|
+
installAgentRouterFetchHook();
|
|
1181
1478
|
function getEffectiveApiKey(): string {
|
|
1182
1479
|
const cfg = loadConfig();
|
|
1183
1480
|
return normalizeApiKey(process.env.AGENTROUTER_API_KEY || process.env.AGENT_ROUTER_API_KEY || cfg.apiKey || "");
|
|
@@ -1339,27 +1636,38 @@ export default function (pi: ExtensionAPI) {
|
|
|
1339
1636
|
if (Array.isArray(payload.system)) payload.system = structuredClone(payload.system);
|
|
1340
1637
|
const isDeepSeek = isDeepSeekRequest(event, ctx);
|
|
1341
1638
|
|
|
1342
|
-
if (payload
|
|
1343
|
-
|
|
1344
|
-
|
|
1639
|
+
if (isCompactionPayload(payload)) {
|
|
1640
|
+
sanitizeCompactionPayload(payload);
|
|
1641
|
+
const messages = Array.isArray(payload.messages) ? payload.messages : payload.input;
|
|
1642
|
+
if (Array.isArray(messages) && messages.length > 0) {
|
|
1643
|
+
normalizeMessagesForAgentRouter(messages, isDeepSeek);
|
|
1644
|
+
if (isDeepSeek) {
|
|
1645
|
+
frameUserTurnsForDeepSeek(messages);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
} else {
|
|
1649
|
+
if (payload.system !== undefined) {
|
|
1650
|
+
payload.system = enforceCanonicalRootPrompt(payload.system);
|
|
1651
|
+
}
|
|
1345
1652
|
|
|
1346
|
-
|
|
1653
|
+
applyPoisonRedaction(payload);
|
|
1347
1654
|
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1655
|
+
const messages = Array.isArray(payload.messages) ? payload.messages : payload.input;
|
|
1656
|
+
if (Array.isArray(messages) && messages.length > 0) {
|
|
1657
|
+
normalizeMessagesForAgentRouter(messages, isDeepSeek);
|
|
1351
1658
|
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1659
|
+
if (isDeepSeek) {
|
|
1660
|
+
frameUserTurnsForDeepSeek(messages);
|
|
1661
|
+
}
|
|
1355
1662
|
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1663
|
+
const firstMsg = messages[0];
|
|
1664
|
+
if (firstMsg && (firstMsg.role === "system" || firstMsg.role === "developer")) {
|
|
1665
|
+
firstMsg.role = "system";
|
|
1666
|
+
if (typeof firstMsg.content === "string") {
|
|
1667
|
+
firstMsg.content = enforceCanonicalRootPrompt(firstMsg.content);
|
|
1668
|
+
} else if (Array.isArray(firstMsg.content)) {
|
|
1669
|
+
firstMsg.content = enforceCanonicalRootPrompt(firstMsg.content);
|
|
1670
|
+
}
|
|
1363
1671
|
}
|
|
1364
1672
|
}
|
|
1365
1673
|
}
|
|
@@ -1767,7 +2075,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1767
2075
|
}
|
|
1768
2076
|
|
|
1769
2077
|
ctx.ui.notify(
|
|
1770
|
-
`[AgentRouter Plugin v2.
|
|
2078
|
+
`[AgentRouter Plugin v2.2.0]\n` +
|
|
1771
2079
|
`- Active model: ${activeModel?.id || "none"} (${isAR ? "AgentRouter [yes]" : "Other Provider"})\n` +
|
|
1772
2080
|
`- Package Priority: ${priorityStatus}\n` +
|
|
1773
2081
|
`- API Key: ${maskedKey}\n` +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@madgagarin/pi-agentrouter",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Official Pi Coding Agent extension for AgentRouter (agentrouter.org). Connects GPT-6 Astra, GPT-5.6 Sol, Claude Opus 5, DeepSeek V4 Flash, and GLM 5.3 with live USD pricing, auto-sync settings, batch quota probe, prompt caching, and WAF protection.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|