@madgagarin/pi-agentrouter 2.1.2 → 2.1.3
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 +2 -1
- package/index.ts +124 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,7 +36,8 @@ pi install npm:@madgagarin/pi-agentrouter
|
|
|
36
36
|
|
|
37
37
|
## Features
|
|
38
38
|
|
|
39
|
-
- **DeepSeek Multi-Turn Tool Calling:** Seamlessly
|
|
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
|
+
- **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.
|
|
40
41
|
- **Model Synchronization:** Automatically registers and adds active models to `enabledModels` in `settings.json` for quick selection via `Ctrl+P`.
|
|
41
42
|
- **Schema Sanitization:** Automatically normalizes tool definitions (e.g. converting `required: null` to empty arrays) for strict OpenAI schema validation compatibility.
|
|
42
43
|
- **WAF Diagnostics & Safe Redaction:** Intercepts upstream blocks and safely redacts older messages while preserving thinking placeholders for reasoning models.
|
package/index.ts
CHANGED
|
@@ -58,7 +58,11 @@ export const KNOWN_MODEL_SPECS: Record<string, ModelSpec> = {
|
|
|
58
58
|
contextWindow: 1048576,
|
|
59
59
|
maxTokens: 65536,
|
|
60
60
|
reasoning: true,
|
|
61
|
-
compat: {
|
|
61
|
+
compat: {
|
|
62
|
+
sendSessionAffinityHeaders: true,
|
|
63
|
+
requiresReasoningContentOnAssistantMessages: true,
|
|
64
|
+
thinkingFormat: "deepseek",
|
|
65
|
+
},
|
|
62
66
|
cost: { input: 4.0 / 1_000_000, output: 12.0 / 1_000_000, cacheRead: 2.0 / 1_000_000, cacheWrite: 0 },
|
|
63
67
|
},
|
|
64
68
|
"deepseek-v4f": {
|
|
@@ -68,7 +72,11 @@ export const KNOWN_MODEL_SPECS: Record<string, ModelSpec> = {
|
|
|
68
72
|
contextWindow: 1048576,
|
|
69
73
|
maxTokens: 65536,
|
|
70
74
|
reasoning: true,
|
|
71
|
-
compat: {
|
|
75
|
+
compat: {
|
|
76
|
+
sendSessionAffinityHeaders: true,
|
|
77
|
+
requiresReasoningContentOnAssistantMessages: true,
|
|
78
|
+
thinkingFormat: "deepseek",
|
|
79
|
+
},
|
|
72
80
|
cost: { input: 4.0 / 1_000_000, output: 12.0 / 1_000_000, cacheRead: 2.0 / 1_000_000, cacheWrite: 0 },
|
|
73
81
|
},
|
|
74
82
|
"glm-5.3": {
|
|
@@ -340,6 +348,23 @@ export function isAgentRouter(providerName?: string, baseUrl?: string): boolean
|
|
|
340
348
|
export const CANONICAL_PI_HEADER =
|
|
341
349
|
"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.";
|
|
342
350
|
|
|
351
|
+
export const LANGUAGE_PREAMBLE =
|
|
352
|
+
"[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
|
+
|
|
354
|
+
export function sanitizeDeepSeekText(text: string): string {
|
|
355
|
+
if (typeof text !== "string") return text;
|
|
356
|
+
// Replace false-positive blocked Russian word in AgentRouter upstream WAF
|
|
357
|
+
return text.replace(/Ключевое/g, "Главное").replace(/ключевое/g, "главное");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function cleanContent(text: string): string {
|
|
361
|
+
if (typeof text !== "string") return text;
|
|
362
|
+
return text
|
|
363
|
+
.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "")
|
|
364
|
+
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "")
|
|
365
|
+
.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "");
|
|
366
|
+
}
|
|
367
|
+
|
|
343
368
|
export function enforceCanonicalRootPrompt(systemPrompt: string | any[] | undefined): string | any[] {
|
|
344
369
|
if (!systemPrompt) {
|
|
345
370
|
return CANONICAL_PI_HEADER;
|
|
@@ -390,6 +415,60 @@ export const WAF_BLOCK_RE = /sensitive[_ ]words?[_ ]detected|content-blocked/i;
|
|
|
390
415
|
export const SENSITIVE_WORDS_RE = /sensitive[_ ]words?[_ ]detected/i;
|
|
391
416
|
export const REDACTED_NOTE = "[Message withheld by local policy]";
|
|
392
417
|
|
|
418
|
+
export function frameUserTurnsForDeepSeek(messages: any[]): void {
|
|
419
|
+
if (!Array.isArray(messages)) return;
|
|
420
|
+
for (const msg of messages) {
|
|
421
|
+
if (!msg || typeof msg !== "object") continue;
|
|
422
|
+
if (msg.role !== "user") continue;
|
|
423
|
+
|
|
424
|
+
if (typeof msg.content === "string") {
|
|
425
|
+
const cleaned = sanitizeDeepSeekText(cleanContent(msg.content));
|
|
426
|
+
if (cleaned === REDACTED_NOTE) {
|
|
427
|
+
msg.content = REDACTED_NOTE;
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (!cleaned.startsWith(LANGUAGE_PREAMBLE)) {
|
|
431
|
+
msg.content = cleaned ? `${LANGUAGE_PREAMBLE}\n\n${cleaned}` : LANGUAGE_PREAMBLE;
|
|
432
|
+
} else {
|
|
433
|
+
msg.content = cleaned;
|
|
434
|
+
}
|
|
435
|
+
} else if (Array.isArray(msg.content)) {
|
|
436
|
+
if (msg.content.length === 0) {
|
|
437
|
+
msg.content.push({ type: "text", text: LANGUAGE_PREAMBLE });
|
|
438
|
+
} else {
|
|
439
|
+
const first = msg.content[0];
|
|
440
|
+
if (first && typeof first === "object" && first.type === "tool_result") {
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
let added = false;
|
|
444
|
+
for (const block of msg.content) {
|
|
445
|
+
if (block && typeof block === "object" && (block.type === "text" || block.type === "input_text")) {
|
|
446
|
+
if (typeof block.text === "string") {
|
|
447
|
+
const cleaned = sanitizeDeepSeekText(cleanContent(block.text));
|
|
448
|
+
if (cleaned === REDACTED_NOTE) {
|
|
449
|
+
block.text = REDACTED_NOTE;
|
|
450
|
+
added = true;
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
if (!cleaned.startsWith(LANGUAGE_PREAMBLE)) {
|
|
454
|
+
block.text = `${LANGUAGE_PREAMBLE}\n\n${cleaned}`;
|
|
455
|
+
} else {
|
|
456
|
+
block.text = cleaned;
|
|
457
|
+
}
|
|
458
|
+
added = true;
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (!added && msg.content.length > 0 && msg.content[0].type !== "tool_result") {
|
|
464
|
+
msg.content.unshift({ type: "text", text: LANGUAGE_PREAMBLE });
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
|
|
393
472
|
export function cleanJsonSchemaObject(schema: any): void {
|
|
394
473
|
if (!schema || typeof schema !== "object") return;
|
|
395
474
|
|
|
@@ -810,6 +889,14 @@ export function normalizeMessagesForAgentRouter(messages: any[], isDeepSeek: boo
|
|
|
810
889
|
msg.reasoning_content = extractedThinking;
|
|
811
890
|
}
|
|
812
891
|
|
|
892
|
+
if (msg.content === null || msg.content === undefined) {
|
|
893
|
+
msg.content = "";
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
if (typeof msg.content === "string" && msg.content.includes("[Tool Call]:")) {
|
|
897
|
+
msg.content = msg.content.replace(/\[Tool Call\]:[^\n]+(\n|$)/g, "").trim();
|
|
898
|
+
}
|
|
899
|
+
|
|
813
900
|
// If assistant executed tool calls, AgentRouter DeepSeek proxy strictly requires reasoning_content
|
|
814
901
|
if (
|
|
815
902
|
Array.isArray(msg.tool_calls) &&
|
|
@@ -832,29 +919,36 @@ export function normalizeMessagesForAgentRouter(messages: any[], isDeepSeek: boo
|
|
|
832
919
|
// Flatten past assistant tool_calls into text and convert tool roles into user turns.
|
|
833
920
|
// This prevents AgentRouter's upstream Anthropic gateway from rejecting the request with:
|
|
834
921
|
// "400: The `content[].thinking` in the thinking mode must be passed back to the API."
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
msg.
|
|
843
|
-
delete msg.tool_calls;
|
|
844
|
-
if (!msg.reasoning_content || (typeof msg.reasoning_content === "string" && !msg.reasoning_content.trim())) {
|
|
845
|
-
msg.reasoning_content = "Executing tools...";
|
|
846
|
-
}
|
|
922
|
+
// Ensure assistant tool calls retain non-empty reasoning_content for gateway compatibility
|
|
923
|
+
if (
|
|
924
|
+
msg.role === "assistant" &&
|
|
925
|
+
Array.isArray(msg.tool_calls) &&
|
|
926
|
+
msg.tool_calls.length > 0 &&
|
|
927
|
+
(!msg.reasoning_content || (typeof msg.reasoning_content === "string" && !msg.reasoning_content.trim()))
|
|
928
|
+
) {
|
|
929
|
+
msg.reasoning_content = "Executing tools...";
|
|
847
930
|
}
|
|
848
931
|
|
|
849
|
-
if (
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
msg.
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
932
|
+
if (typeof msg.content === "string") {
|
|
933
|
+
msg.content = sanitizeDeepSeekText(msg.content);
|
|
934
|
+
} else if (Array.isArray(msg.content)) {
|
|
935
|
+
for (const block of msg.content) {
|
|
936
|
+
if (block && typeof block === "object") {
|
|
937
|
+
if (typeof block.text === "string") {
|
|
938
|
+
block.text = sanitizeDeepSeekText(block.text);
|
|
939
|
+
}
|
|
940
|
+
if (typeof block.content === "string") {
|
|
941
|
+
block.content = sanitizeDeepSeekText(block.content);
|
|
942
|
+
}
|
|
943
|
+
if (typeof block.thinking === "string") {
|
|
944
|
+
block.thinking = sanitizeDeepSeekText(block.thinking);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
if (typeof msg.reasoning_content === "string") {
|
|
950
|
+
msg.reasoning_content = sanitizeDeepSeekText(msg.reasoning_content);
|
|
856
951
|
}
|
|
857
|
-
|
|
858
952
|
}
|
|
859
953
|
}
|
|
860
954
|
}
|
|
@@ -1240,6 +1334,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
1240
1334
|
|
|
1241
1335
|
const payload = event.payload;
|
|
1242
1336
|
if (payload) {
|
|
1337
|
+
if (Array.isArray(payload.messages)) payload.messages = structuredClone(payload.messages);
|
|
1338
|
+
if (Array.isArray(payload.input)) payload.input = structuredClone(payload.input);
|
|
1339
|
+
if (Array.isArray(payload.system)) payload.system = structuredClone(payload.system);
|
|
1243
1340
|
const isDeepSeek = isDeepSeekRequest(event, ctx);
|
|
1244
1341
|
|
|
1245
1342
|
if (payload.system !== undefined) {
|
|
@@ -1252,6 +1349,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
1252
1349
|
if (Array.isArray(messages) && messages.length > 0) {
|
|
1253
1350
|
normalizeMessagesForAgentRouter(messages, isDeepSeek);
|
|
1254
1351
|
|
|
1352
|
+
if (isDeepSeek) {
|
|
1353
|
+
frameUserTurnsForDeepSeek(messages);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1255
1356
|
const firstMsg = messages[0];
|
|
1256
1357
|
if (firstMsg && (firstMsg.role === "system" || firstMsg.role === "developer")) {
|
|
1257
1358
|
firstMsg.role = "system";
|
|
@@ -1666,7 +1767,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1666
1767
|
}
|
|
1667
1768
|
|
|
1668
1769
|
ctx.ui.notify(
|
|
1669
|
-
`[AgentRouter Plugin v2.1.
|
|
1770
|
+
`[AgentRouter Plugin v2.1.3]\n` +
|
|
1670
1771
|
`- Active model: ${activeModel?.id || "none"} (${isAR ? "AgentRouter [yes]" : "Other Provider"})\n` +
|
|
1671
1772
|
`- Package Priority: ${priorityStatus}\n` +
|
|
1672
1773
|
`- API Key: ${maskedKey}\n` +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@madgagarin/pi-agentrouter",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.3",
|
|
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"
|