@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.
Files changed (3) hide show
  1. package/README.md +2 -1
  2. package/index.ts +124 -23
  3. 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 flattens multi-turn tool history and preserves `reasoning_content` across tool execution turns, completely preventing upstream gateway 400 thinking mode errors.
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: { sendSessionAffinityHeaders: true },
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: { sendSessionAffinityHeaders: true },
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
- if (msg.role === "assistant" && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
836
- let contentStr = typeof msg.content === "string" ? msg.content : "";
837
- for (const tc of msg.tool_calls) {
838
- const fnName = tc?.function?.name || tc?.name || "tool";
839
- const fnArgs = tc?.function?.arguments || "{}";
840
- contentStr += (contentStr ? "\n" : "") + `[Tool Call]: ${fnName}(${fnArgs})`;
841
- }
842
- msg.content = contentStr;
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 (msg.role === "tool" || msg.role === "toolResult") {
850
- const text = typeof msg.content === "string" ? msg.content : "";
851
- const toolName = (msg as any).toolName || (msg as any).name || "tool";
852
- msg.role = "user";
853
- msg.content = `[Tool Result for ${toolName}]:\n${text}`;
854
- delete msg.tool_call_id;
855
- delete (msg as any).toolCallId;
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.2]\n` +
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.2",
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"