@madgagarin/pi-agentrouter 2.1.1 → 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 +10 -10
- package/index.ts +681 -47
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,15 +36,15 @@ pi install npm:@madgagarin/pi-agentrouter
|
|
|
36
36
|
|
|
37
37
|
## Features
|
|
38
38
|
|
|
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.
|
|
39
41
|
- **Model Synchronization:** Automatically registers and adds active models to `enabledModels` in `settings.json` for quick selection via `Ctrl+P`.
|
|
40
|
-
- **DeepSeek Multi-Turn Tool Calling:** Preserves `reasoning_content` and handles thinking blocks across multi-step tool execution, avoiding API 400 errors.
|
|
41
42
|
- **Schema Sanitization:** Automatically normalizes tool definitions (e.g. converting `required: null` to empty arrays) for strict OpenAI schema validation compatibility.
|
|
42
|
-
- **WAF Diagnostics:** Intercepts upstream
|
|
43
|
-
- **Dual Endpoint Protocols:** Supports both OpenAI (`agentrouter-openai`) and Anthropic Messages API (`agentrouter-clode`) routes for models like `deepseek-v4-flash`.
|
|
43
|
+
- **WAF Diagnostics & Safe Redaction:** Intercepts upstream blocks and safely redacts older messages while preserving thinking placeholders for reasoning models.
|
|
44
44
|
- **Isolated Credential Storage:** Manages API keys exclusively within `agentrouter-*` provider namespaces in `auth.json` without modifying default third-party provider keys.
|
|
45
45
|
- **Live Pricing & Quota Probing:** Fetches current rates from the gateway API on startup and provides `/agentrouter check` to probe model availability and track usage.
|
|
46
46
|
- **Subagent Rate Pacing:** Uses a file-based lock (`~/.pi/agent/.agentrouter-pacing`) across concurrent subagents to prevent 429 rate limit errors.
|
|
47
|
-
- **Prompt Caching Compatibility:** Preserves affinity headers and
|
|
47
|
+
- **Prompt Caching Compatibility:** Preserves affinity headers and pricing for prompt caching reuse.
|
|
48
48
|
|
|
49
49
|
---
|
|
50
50
|
|
|
@@ -52,12 +52,12 @@ pi install npm:@madgagarin/pi-agentrouter
|
|
|
52
52
|
|
|
53
53
|
Rates are fetched from the [agentrouter.org](https://agentrouter.org) gateway API ($2.00 / 1M tokens base unit):
|
|
54
54
|
|
|
55
|
-
| Model | Provider | Context | Output | Reasoning | Input / 1M | Output / 1M | Quota Policy |
|
|
56
|
-
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
|
|
57
|
-
| `deepseek-v4-flash` | `agentrouter-openai`
|
|
58
|
-
| `glm-5.3` | `agentrouter-openai` | 1M | 128K | Yes | $3.00 | $12.00 | Unlimited |
|
|
59
|
-
| `gpt-6-astra` | `agentrouter-openai` | 1M | 128K | Yes | $3.00 | $15.00 | Daily batch drops |
|
|
60
|
-
| `gpt-5.6-sol` | `agentrouter-openai` | 1M | 128K | Yes | $3.00 | $15.00 | Daily batch drops |
|
|
55
|
+
| Model | Provider | Context | Output | Reasoning | Input / 1M | Output / 1M | Cache Read / 1M | Quota Policy |
|
|
56
|
+
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
|
|
57
|
+
| `deepseek-v4-flash` | `agentrouter-openai` | 1M | 64K | Yes | $4.00 | $12.00 | $2.00 | Unlimited |
|
|
58
|
+
| `glm-5.3` | `agentrouter-openai` | 1M | 128K | Yes | $3.00 | $12.00 | - | Unlimited |
|
|
59
|
+
| `gpt-6-astra` | `agentrouter-openai` | 1M | 128K | Yes | $3.00 | $15.00 | - | Daily batch drops |
|
|
60
|
+
| `gpt-5.6-sol` | `agentrouter-openai` | 1M | 128K | Yes | $3.00 | $15.00 | - | Daily batch drops |
|
|
61
61
|
| `claude-opus-5` | `agentrouter-clode` | 1M | 64K | Yes (Adaptive) | $6.00 | $30.00 | Daily batch drops |
|
|
62
62
|
| `claude-opus-4-8` | `agentrouter-clode` | 1M | 64K | Yes (Adaptive) | $8.00 | $40.00 | Daily batch drops |
|
|
63
63
|
|
package/index.ts
CHANGED
|
@@ -58,8 +58,12 @@ export const KNOWN_MODEL_SPECS: Record<string, ModelSpec> = {
|
|
|
58
58
|
contextWindow: 1048576,
|
|
59
59
|
maxTokens: 65536,
|
|
60
60
|
reasoning: true,
|
|
61
|
-
compat: {
|
|
62
|
-
|
|
61
|
+
compat: {
|
|
62
|
+
sendSessionAffinityHeaders: true,
|
|
63
|
+
requiresReasoningContentOnAssistantMessages: true,
|
|
64
|
+
thinkingFormat: "deepseek",
|
|
65
|
+
},
|
|
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": {
|
|
65
69
|
id: "deepseek-v4f",
|
|
@@ -68,8 +72,12 @@ export const KNOWN_MODEL_SPECS: Record<string, ModelSpec> = {
|
|
|
68
72
|
contextWindow: 1048576,
|
|
69
73
|
maxTokens: 65536,
|
|
70
74
|
reasoning: true,
|
|
71
|
-
compat: {
|
|
72
|
-
|
|
75
|
+
compat: {
|
|
76
|
+
sendSessionAffinityHeaders: true,
|
|
77
|
+
requiresReasoningContentOnAssistantMessages: true,
|
|
78
|
+
thinkingFormat: "deepseek",
|
|
79
|
+
},
|
|
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": {
|
|
75
83
|
id: "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;
|
|
@@ -377,6 +402,73 @@ export function enforceCanonicalRootPrompt(systemPrompt: string | any[] | undefi
|
|
|
377
402
|
return systemPrompt;
|
|
378
403
|
}
|
|
379
404
|
|
|
405
|
+
export function isDeepSeekRequest(event: any, ctx: any): boolean {
|
|
406
|
+
const modelId = (
|
|
407
|
+
(event as any)?.model?.id ||
|
|
408
|
+
(ctx as any)?.model?.id ||
|
|
409
|
+
(event as any)?.payload?.model ||
|
|
410
|
+
""
|
|
411
|
+
).toLowerCase();
|
|
412
|
+
return modelId.includes("deepseek");
|
|
413
|
+
}
|
|
414
|
+
export const WAF_BLOCK_RE = /sensitive[_ ]words?[_ ]detected|content-blocked/i;
|
|
415
|
+
export const SENSITIVE_WORDS_RE = /sensitive[_ ]words?[_ ]detected/i;
|
|
416
|
+
export const REDACTED_NOTE = "[Message withheld by local policy]";
|
|
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
|
+
|
|
380
472
|
export function cleanJsonSchemaObject(schema: any): void {
|
|
381
473
|
if (!schema || typeof schema !== "object") return;
|
|
382
474
|
|
|
@@ -414,8 +506,351 @@ export function sanitizeOpenAiTools(tools: any[]): void {
|
|
|
414
506
|
}
|
|
415
507
|
}
|
|
416
508
|
|
|
417
|
-
export function
|
|
509
|
+
export function isRecord(v: unknown): v is Record<string, unknown> {
|
|
510
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export const redactSet = new Set<string>();
|
|
514
|
+
export let escalatePending = false;
|
|
515
|
+
export let isSensitiveBlock = false;
|
|
516
|
+
export let exhausted = false;
|
|
517
|
+
export let sessionAnchor: string | null = null;
|
|
518
|
+
export let wafNotified = false;
|
|
519
|
+
|
|
520
|
+
export function resetPoisonRedactionState(): void {
|
|
521
|
+
redactSet.clear();
|
|
522
|
+
escalatePending = false;
|
|
523
|
+
isSensitiveBlock = false;
|
|
524
|
+
exhausted = false;
|
|
525
|
+
sessionAnchor = null;
|
|
526
|
+
wafNotified = false;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
export function triggerEscalation(sensitive: boolean = false): void {
|
|
530
|
+
escalatePending = true;
|
|
531
|
+
isSensitiveBlock = sensitive;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
export function fingerprintOf(msg: Record<string, unknown>): string {
|
|
536
|
+
const tc = Array.isArray(msg.tool_calls) ? JSON.stringify(msg.tool_calls).slice(0, 80) : "";
|
|
537
|
+
if (msg.type === "function_call" || msg.type === "function_call_output") {
|
|
538
|
+
return `${msg.type}:${(msg as any).call_id}:${JSON.stringify((msg as any).arguments ?? (msg as any).output ?? "").slice(0, 160)}`;
|
|
539
|
+
}
|
|
540
|
+
return `${msg.role ?? msg.type}:${JSON.stringify(msg.content ?? "").slice(0, 160)}:${tc}`;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export function firstUserAnchor(messages: unknown[]): string {
|
|
544
|
+
for (const m of messages) {
|
|
545
|
+
if (isRecord(m) && isHumanUserMessage(m as Record<string, unknown>)) return fingerprintOf(m);
|
|
546
|
+
}
|
|
547
|
+
return String(messages.length);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export function isTextBlock(block: Record<string, unknown>): boolean {
|
|
551
|
+
return block.type === "text" || block.type === "input_text" || block.type === "output_text";
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export function isHumanUserMessage(msg: Record<string, unknown>): boolean {
|
|
555
|
+
if (msg.role !== "user") return false;
|
|
556
|
+
if (typeof msg.content === "string") return true;
|
|
557
|
+
if (Array.isArray(msg.content)) {
|
|
558
|
+
const hasToolResult = msg.content.some((b) => isRecord(b) && b.type === "tool_result");
|
|
559
|
+
const hasHumanText = msg.content.some(
|
|
560
|
+
(b) =>
|
|
561
|
+
isRecord(b) &&
|
|
562
|
+
(b.type === "text" || b.type === "input_text") &&
|
|
563
|
+
typeof b.text === "string" &&
|
|
564
|
+
b.text.trim().length > 0 &&
|
|
565
|
+
b.text !== REDACTED_NOTE
|
|
566
|
+
);
|
|
567
|
+
if (hasToolResult && !hasHumanText) return false;
|
|
568
|
+
return true;
|
|
569
|
+
}
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export function lastHumanUserIndex(messages: unknown[]): number {
|
|
574
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
575
|
+
if (isRecord(messages[i]) && isHumanUserMessage(messages[i] as Record<string, unknown>)) {
|
|
576
|
+
return i;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return -1;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export function hasRedactableText(content: unknown, msg?: Record<string, unknown>): boolean {
|
|
583
|
+
if (msg) {
|
|
584
|
+
if (msg.details && (typeof msg.details === "string" || Object.keys(msg.details).length > 0)) return true;
|
|
585
|
+
if (typeof (msg as any).reasoning_content === "string" && (msg as any).reasoning_content.length > 0) return true;
|
|
586
|
+
if (typeof (msg as any).thinking === "string" && (msg as any).thinking.length > 0) return true;
|
|
587
|
+
}
|
|
588
|
+
if (msg?.type === "function_call") return typeof (msg as any).arguments === "string" && (msg as any).arguments !== "{}";
|
|
589
|
+
if (msg?.type === "function_call_output") return hasRedactableText((msg as any).output);
|
|
590
|
+
if (typeof content === "string") return content.length > 0 && content !== REDACTED_NOTE;
|
|
591
|
+
if (Array.isArray(content)) {
|
|
592
|
+
for (const b of content) {
|
|
593
|
+
if (!isRecord(b)) continue;
|
|
594
|
+
if (b.type === "thinking" || b.type === "reasoning") return true;
|
|
595
|
+
if (isTextBlock(b) && typeof b.text === "string" && b.text.length > 0 && b.text !== REDACTED_NOTE) return true;
|
|
596
|
+
if (b.type === "tool_result") {
|
|
597
|
+
if (typeof b.content === "string" && b.content.length > 0 && b.content !== REDACTED_NOTE) return true;
|
|
598
|
+
if (Array.isArray(b.content)) {
|
|
599
|
+
for (const c of b.content) {
|
|
600
|
+
if (isRecord(c) && isTextBlock(c) && typeof c.text === "string" && c.text.length > 0 && c.text !== REDACTED_NOTE) {
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (b.type === "tool_use" && isRecord(b.input) && Object.keys(b.input).length > 0) {
|
|
607
|
+
return true;
|
|
608
|
+
}
|
|
609
|
+
if (b.type === "toolCall" && isRecord(b.arguments) && Object.keys(b.arguments).length > 0) {
|
|
610
|
+
return true;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (msg && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
|
|
615
|
+
for (const tc of msg.tool_calls) {
|
|
616
|
+
if (isRecord(tc) && isRecord(tc.function) && typeof tc.function.arguments === "string" && tc.function.arguments !== "{}") {
|
|
617
|
+
return true;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return false;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
export function redactBlocks(content: unknown): void {
|
|
625
|
+
if (!Array.isArray(content)) return;
|
|
626
|
+
for (let idx = content.length - 1; idx >= 0; idx--) {
|
|
627
|
+
const block = content[idx];
|
|
628
|
+
if (!isRecord(block)) continue;
|
|
629
|
+
|
|
630
|
+
// For thinking / reasoning blocks, keep a harmless placeholder rather than deleting,
|
|
631
|
+
// because thinking models (like DeepSeek) strictly require thinking blocks to be present in multi-turn history
|
|
632
|
+
if (block.type === "thinking" || block.type === "reasoning") {
|
|
633
|
+
if (typeof (block as any).thinking === "string") {
|
|
634
|
+
(block as any).thinking = "Thinking...";
|
|
635
|
+
}
|
|
636
|
+
if (typeof (block as any).text === "string") {
|
|
637
|
+
(block as any).text = "Thinking...";
|
|
638
|
+
}
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (isTextBlock(block) && typeof block.text === "string") {
|
|
643
|
+
block.text = REDACTED_NOTE;
|
|
644
|
+
} else if (block.type === "tool_result") {
|
|
645
|
+
if (typeof block.content === "string") {
|
|
646
|
+
block.content = REDACTED_NOTE;
|
|
647
|
+
} else if (Array.isArray(block.content)) {
|
|
648
|
+
for (const b of block.content) {
|
|
649
|
+
if (isRecord(b) && isTextBlock(b) && typeof b.text === "string") {
|
|
650
|
+
b.text = REDACTED_NOTE;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
} else if (block.type === "tool_use") {
|
|
655
|
+
block.input = {};
|
|
656
|
+
} else if (block.type === "toolCall") {
|
|
657
|
+
block.arguments = {};
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
export function isHideable(msg: Record<string, unknown>): boolean {
|
|
663
|
+
return (
|
|
664
|
+
msg.role === "user" ||
|
|
665
|
+
msg.role === "assistant" ||
|
|
666
|
+
msg.role === "tool" ||
|
|
667
|
+
msg.role === "toolResult" ||
|
|
668
|
+
msg.type === "function_call" ||
|
|
669
|
+
msg.type === "function_call_output"
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
export function redactMessageAt(messages: unknown[], i: number): void {
|
|
674
|
+
const msg = messages[i];
|
|
675
|
+
if (!isRecord(msg)) return;
|
|
676
|
+
|
|
677
|
+
// Clear tool result details / raw diffs / attachments
|
|
678
|
+
if ("details" in msg) delete msg.details;
|
|
679
|
+
// DeepSeek and reasoning models require reasoning_content/thinking to exist in thinking mode.
|
|
680
|
+
// Never delete them completely; keep a sanitized minimal placeholder.
|
|
681
|
+
if ("reasoning_content" in msg) {
|
|
682
|
+
(msg as any).reasoning_content = "Thinking...";
|
|
683
|
+
}
|
|
684
|
+
if ("thinking" in msg) {
|
|
685
|
+
(msg as any).thinking = "Thinking...";
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (msg.type === "function_call") {
|
|
689
|
+
(msg as any).arguments = "{}";
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
if (msg.type === "function_call_output") {
|
|
693
|
+
if (typeof (msg as any).output === "string") (msg as any).output = REDACTED_NOTE;
|
|
694
|
+
else redactBlocks((msg as any).output);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
if (typeof msg.content === "string") {
|
|
698
|
+
msg.content = REDACTED_NOTE;
|
|
699
|
+
} else if (Array.isArray(msg.content)) {
|
|
700
|
+
redactBlocks(msg.content);
|
|
701
|
+
if (msg.content.length === 0 && !Array.isArray(msg.tool_calls)) {
|
|
702
|
+
msg.content = [{ type: "text", text: REDACTED_NOTE }];
|
|
703
|
+
}
|
|
704
|
+
} else if (msg.role === "tool" || msg.role === "toolResult" || msg.role === "assistant") {
|
|
705
|
+
msg.content = REDACTED_NOTE;
|
|
706
|
+
}
|
|
707
|
+
if (Array.isArray(msg.tool_calls)) {
|
|
708
|
+
for (const tc of msg.tool_calls) {
|
|
709
|
+
if (isRecord(tc) && isRecord(tc.function) && typeof tc.function.arguments === "string") {
|
|
710
|
+
tc.function.arguments = "{}";
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
export function applyPoisonRedaction(payload: Record<string, unknown>): void {
|
|
717
|
+
const messages = Array.isArray(payload.messages) ? payload.messages : (payload as any).input;
|
|
718
|
+
if (!Array.isArray(messages) || messages.length === 0) return;
|
|
719
|
+
|
|
720
|
+
// Prune failed assistant messages carrying error status/text in-place so dead error turns do not linger
|
|
721
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
722
|
+
const m = messages[i];
|
|
723
|
+
if (!isRecord(m)) continue;
|
|
724
|
+
if (m.role === "assistant") {
|
|
725
|
+
if (m.stopReason === "error") {
|
|
726
|
+
messages.splice(i, 1);
|
|
727
|
+
} else if (typeof m.content === "string" && WAF_BLOCK_RE.test(m.content)) {
|
|
728
|
+
messages.splice(i, 1);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const fps = messages.map((m) => (isRecord(m) ? fingerprintOf(m) : ""));
|
|
734
|
+
|
|
735
|
+
const anchor = firstUserAnchor(messages);
|
|
736
|
+
if (anchor !== sessionAnchor) {
|
|
737
|
+
const firstContact = sessionAnchor === null;
|
|
738
|
+
sessionAnchor = anchor;
|
|
739
|
+
if (!firstContact) {
|
|
740
|
+
redactSet.clear();
|
|
741
|
+
escalatePending = false;
|
|
742
|
+
isSensitiveBlock = false;
|
|
743
|
+
exhausted = false;
|
|
744
|
+
wafNotified = false;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if (redactSet.size > 0 && !fps.some((fp) => fp && redactSet.has(fp))) {
|
|
749
|
+
redactSet.clear();
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const lastHumanUser = lastHumanUserIndex(messages);
|
|
753
|
+
|
|
754
|
+
for (let i = 0; i < messages.length; i++) {
|
|
755
|
+
if (i === lastHumanUser) continue;
|
|
756
|
+
if (!isRecord(messages[i]) || !isHideable(messages[i] as Record<string, unknown>)) continue;
|
|
757
|
+
if (redactSet.has(fps[i])) redactMessageAt(messages, i);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (escalatePending) {
|
|
761
|
+
escalatePending = false;
|
|
762
|
+
const sensitive = isSensitiveBlock;
|
|
763
|
+
isSensitiveBlock = false;
|
|
764
|
+
|
|
765
|
+
if (sensitive) {
|
|
766
|
+
// Sensitive words detected: neutralize ALL turns (older user, tool, assistant, active tool results) except active human user
|
|
767
|
+
for (let i = 0; i < messages.length; i++) {
|
|
768
|
+
if (i === lastHumanUser) continue;
|
|
769
|
+
if (!isRecord(messages[i])) continue;
|
|
770
|
+
const m = messages[i] as Record<string, unknown>;
|
|
771
|
+
if (!isHideable(m)) continue;
|
|
772
|
+
redactSet.add(fps[i]);
|
|
773
|
+
if (hasRedactableText(m.content, m)) {
|
|
774
|
+
redactMessageAt(messages, i);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
} else {
|
|
778
|
+
// Language ratio block: Stage 1 older user turns, Stage 2 assistant & tool
|
|
779
|
+
let anyRedacted = false;
|
|
780
|
+
for (let i = 0; i < messages.length; i++) {
|
|
781
|
+
if (i === lastHumanUser) continue;
|
|
782
|
+
if (!isRecord(messages[i])) continue;
|
|
783
|
+
const m = messages[i] as Record<string, unknown>;
|
|
784
|
+
if (m.role !== "user") continue;
|
|
785
|
+
if (redactSet.has(fps[i])) continue;
|
|
786
|
+
redactSet.add(fps[i]);
|
|
787
|
+
if (hasRedactableText(m.content, m)) {
|
|
788
|
+
redactMessageAt(messages, i);
|
|
789
|
+
anyRedacted = true;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
if (!anyRedacted) {
|
|
793
|
+
for (let i = 0; i < messages.length; i++) {
|
|
794
|
+
if (i === lastHumanUser) continue;
|
|
795
|
+
if (!isRecord(messages[i])) continue;
|
|
796
|
+
const m = messages[i] as Record<string, unknown>;
|
|
797
|
+
if (!isHideable(m) || m.role === "user") continue;
|
|
798
|
+
if (redactSet.has(fps[i])) continue;
|
|
799
|
+
redactSet.add(fps[i]);
|
|
800
|
+
if (hasRedactableText(m.content, m)) {
|
|
801
|
+
redactMessageAt(messages, i);
|
|
802
|
+
anyRedacted = true;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// Exhausted if there are no more hideable messages left to redact other than lastHumanUser
|
|
810
|
+
exhausted = true;
|
|
811
|
+
for (let i = 0; i < messages.length; i++) {
|
|
812
|
+
if (i === lastHumanUser) continue;
|
|
813
|
+
if (!isRecord(messages[i]) || !isHideable(messages[i] as Record<string, unknown>)) continue;
|
|
814
|
+
if (!redactSet.has(fps[i])) {
|
|
815
|
+
exhausted = false;
|
|
816
|
+
break;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
export function normalizeMessagesForAgentRouter(messages: any[], isDeepSeek: boolean = false): void {
|
|
418
822
|
if (!Array.isArray(messages)) return;
|
|
823
|
+
|
|
824
|
+
// Prune poisoned/failed assistant turns globally across all models (e.g. prior 402 quota failure, network errors, empty turns)
|
|
825
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
826
|
+
const msg = messages[i];
|
|
827
|
+
if (!msg || typeof msg !== "object") continue;
|
|
828
|
+
if (msg.role === "assistant") {
|
|
829
|
+
const hasToolCalls = Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0;
|
|
830
|
+
const hasToolUseBlocks =
|
|
831
|
+
Array.isArray(msg.content) &&
|
|
832
|
+
msg.content.some((b: any) => b && typeof b === "object" && (b.type === "tool_use" || b.type === "tool_call"));
|
|
833
|
+
const hasTools = hasToolCalls || hasToolUseBlocks;
|
|
834
|
+
const isEmptyContent =
|
|
835
|
+
!msg.content ||
|
|
836
|
+
msg.content === "" ||
|
|
837
|
+
(Array.isArray(msg.content) && (
|
|
838
|
+
msg.content.length === 0 ||
|
|
839
|
+
msg.content.every((b: any) => b && typeof b === "object" && (b.type === "text" || b.type === "input_text") && (!b.text || b.text.trim() === ""))
|
|
840
|
+
));
|
|
841
|
+
|
|
842
|
+
if (
|
|
843
|
+
msg.stopReason === "error" ||
|
|
844
|
+
(msg.stopReason === "aborted" && isEmptyContent) ||
|
|
845
|
+
(typeof msg.content === "string" && WAF_BLOCK_RE.test(msg.content)) ||
|
|
846
|
+
(isEmptyContent && !hasTools && !msg.reasoning_content)
|
|
847
|
+
) {
|
|
848
|
+
messages.splice(i, 1);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
|
|
419
854
|
for (const msg of messages) {
|
|
420
855
|
if (!msg || typeof msg !== "object") continue;
|
|
421
856
|
if (msg.role === "developer") {
|
|
@@ -454,14 +889,173 @@ export function normalizeMessagesForAgentRouter(messages: any[]): void {
|
|
|
454
889
|
msg.reasoning_content = extractedThinking;
|
|
455
890
|
}
|
|
456
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
|
+
|
|
457
900
|
// If assistant executed tool calls, AgentRouter DeepSeek proxy strictly requires reasoning_content
|
|
458
|
-
if (
|
|
901
|
+
if (
|
|
902
|
+
Array.isArray(msg.tool_calls) &&
|
|
903
|
+
msg.tool_calls.length > 0 &&
|
|
904
|
+
(!msg.reasoning_content || (typeof msg.reasoning_content === "string" && !msg.reasoning_content.trim()))
|
|
905
|
+
) {
|
|
906
|
+
msg.reasoning_content = "Executing tools...";
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// If DeepSeek model and content is still empty without tool calls, provide fallback text
|
|
910
|
+
if (isDeepSeek && (!msg.content || msg.content === "") && (!Array.isArray(msg.tool_calls) || msg.tool_calls.length === 0)) {
|
|
911
|
+
msg.content = "[Interrupted]";
|
|
912
|
+
if (!msg.reasoning_content) {
|
|
913
|
+
msg.reasoning_content = "Interrupted response";
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
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
|
+
// 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
|
+
) {
|
|
459
929
|
msg.reasoning_content = "Executing tools...";
|
|
460
930
|
}
|
|
931
|
+
|
|
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);
|
|
951
|
+
}
|
|
461
952
|
}
|
|
462
953
|
}
|
|
463
954
|
}
|
|
464
955
|
|
|
956
|
+
export function cleanupDeepSeekDuplicates(): {
|
|
957
|
+
cleanedSettings: boolean;
|
|
958
|
+
cleanedCache: boolean;
|
|
959
|
+
cleanedModelsJson: boolean;
|
|
960
|
+
} {
|
|
961
|
+
let cleanedSettings = false;
|
|
962
|
+
let cleanedCache = false;
|
|
963
|
+
let cleanedModelsJson = false;
|
|
964
|
+
|
|
965
|
+
// 1. Clean settings.json
|
|
966
|
+
try {
|
|
967
|
+
if (fs.existsSync(SETTINGS_FILE)) {
|
|
968
|
+
const settings = JSON.parse(fs.readFileSync(SETTINGS_FILE, "utf-8"));
|
|
969
|
+
let modified = false;
|
|
970
|
+
|
|
971
|
+
if (Array.isArray(settings.enabledModels)) {
|
|
972
|
+
const clodeIdx = settings.enabledModels.indexOf("agentrouter-clode/deepseek-v4-flash");
|
|
973
|
+
if (clodeIdx !== -1) {
|
|
974
|
+
settings.enabledModels.splice(clodeIdx, 1);
|
|
975
|
+
modified = true;
|
|
976
|
+
}
|
|
977
|
+
// Deduplicate enabledModels
|
|
978
|
+
const seen = new Set<string>();
|
|
979
|
+
const deduped: string[] = [];
|
|
980
|
+
for (const m of settings.enabledModels) {
|
|
981
|
+
if (!seen.has(m)) {
|
|
982
|
+
seen.add(m);
|
|
983
|
+
deduped.push(m);
|
|
984
|
+
} else {
|
|
985
|
+
modified = true;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
settings.enabledModels = deduped;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
if (settings.subagents && typeof settings.subagents === "object" && settings.subagents.agentOverrides) {
|
|
992
|
+
for (const override of Object.values(settings.subagents.agentOverrides)) {
|
|
993
|
+
if ((override as any)?.model === "agentrouter-clode/deepseek-v4-flash") {
|
|
994
|
+
(override as any).model = "agentrouter-openai/deepseek-v4-flash";
|
|
995
|
+
modified = true;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
if (settings.defaultProvider === "agentrouter-clode" && settings.defaultModel === "deepseek-v4-flash") {
|
|
1001
|
+
settings.defaultProvider = "agentrouter-openai";
|
|
1002
|
+
modified = true;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
if (modified) {
|
|
1006
|
+
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), "utf-8");
|
|
1007
|
+
cleanedSettings = true;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
} catch {}
|
|
1011
|
+
|
|
1012
|
+
// 2. Clean MODELS_CACHE_FILE (.agentrouter-models-cache.json)
|
|
1013
|
+
try {
|
|
1014
|
+
if (fs.existsSync(MODELS_CACHE_FILE)) {
|
|
1015
|
+
const cacheData = JSON.parse(fs.readFileSync(MODELS_CACHE_FILE, "utf-8"));
|
|
1016
|
+
if (Array.isArray(cacheData)) {
|
|
1017
|
+
let modified = false;
|
|
1018
|
+
for (const item of cacheData) {
|
|
1019
|
+
if (item && item.model_name === "deepseek-v4-flash") {
|
|
1020
|
+
if (Array.isArray(item.supported_endpoint_types) && item.supported_endpoint_types.includes("anthropic")) {
|
|
1021
|
+
item.supported_endpoint_types = item.supported_endpoint_types.filter((t: string) => t !== "anthropic");
|
|
1022
|
+
modified = true;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
if (modified) {
|
|
1027
|
+
fs.writeFileSync(MODELS_CACHE_FILE, JSON.stringify(cacheData, null, 2), "utf-8");
|
|
1028
|
+
cleanedCache = true;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
} catch {}
|
|
1033
|
+
|
|
1034
|
+
// 3. Clean models.json (if present)
|
|
1035
|
+
try {
|
|
1036
|
+
const modelsJsonPath = path.join(process.env.HOME || "", ".pi/agent/models.json");
|
|
1037
|
+
if (fs.existsSync(modelsJsonPath)) {
|
|
1038
|
+
const modelsJson = JSON.parse(fs.readFileSync(modelsJsonPath, "utf-8"));
|
|
1039
|
+
let modified = false;
|
|
1040
|
+
if (modelsJson?.providers?.["agentrouter-clode"]?.models) {
|
|
1041
|
+
const origLen = modelsJson.providers["agentrouter-clode"].models.length;
|
|
1042
|
+
modelsJson.providers["agentrouter-clode"].models = modelsJson.providers["agentrouter-clode"].models.filter(
|
|
1043
|
+
(m: any) => (typeof m === "string" ? m !== "deepseek-v4-flash" : m?.id !== "deepseek-v4-flash")
|
|
1044
|
+
);
|
|
1045
|
+
if (modelsJson.providers["agentrouter-clode"].models.length !== origLen) {
|
|
1046
|
+
modified = true;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
if (modified) {
|
|
1050
|
+
fs.writeFileSync(modelsJsonPath, JSON.stringify(modelsJson, null, 2), "utf-8");
|
|
1051
|
+
cleanedModelsJson = true;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
} catch {}
|
|
1055
|
+
|
|
1056
|
+
return { cleanedSettings, cleanedCache, cleanedModelsJson };
|
|
1057
|
+
}
|
|
1058
|
+
|
|
465
1059
|
let lastWarnedErrorTimestamp = 0;
|
|
466
1060
|
export function checkAndNotifyContentBlocked(errMessage: string | undefined, ctx: any): void {
|
|
467
1061
|
if (!errMessage) return;
|
|
@@ -481,7 +1075,7 @@ export function checkAndNotifyContentBlocked(errMessage: string | undefined, ctx
|
|
|
481
1075
|
export async function fetchLivePricing(): Promise<ApiPricingModel[] | null> {
|
|
482
1076
|
try {
|
|
483
1077
|
const res = await fetch("https://agentrouter.org/api/pricing", {
|
|
484
|
-
|
|
1078
|
+
|
|
485
1079
|
});
|
|
486
1080
|
if (!res.ok) return null;
|
|
487
1081
|
const data = await res.json();
|
|
@@ -505,8 +1099,7 @@ export async function fetchTokenUsage(apiKey: string): Promise<number | null> {
|
|
|
505
1099
|
{
|
|
506
1100
|
headers: {
|
|
507
1101
|
Authorization: `Bearer ${apiKey}`,
|
|
508
|
-
|
|
509
|
-
},
|
|
1102
|
+
},
|
|
510
1103
|
}
|
|
511
1104
|
);
|
|
512
1105
|
if (!res.ok) return null;
|
|
@@ -532,12 +1125,12 @@ export async function probeModelQuota(modelId: string, apiKey: string, isAnthrop
|
|
|
532
1125
|
"Content-Type": "application/json",
|
|
533
1126
|
"x-api-key": apiKey,
|
|
534
1127
|
"anthropic-version": "2023-06-01",
|
|
535
|
-
"User-Agent":
|
|
1128
|
+
"User-Agent": getPiUserAgent(),
|
|
536
1129
|
}
|
|
537
1130
|
: {
|
|
538
1131
|
"Content-Type": "application/json",
|
|
539
1132
|
Authorization: `Bearer ${apiKey}`,
|
|
540
|
-
"User-Agent":
|
|
1133
|
+
"User-Agent": getPiUserAgent(),
|
|
541
1134
|
};
|
|
542
1135
|
|
|
543
1136
|
const body = isAnthropic
|
|
@@ -629,17 +1222,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
629
1222
|
} else {
|
|
630
1223
|
openaiModels.push(modelObj);
|
|
631
1224
|
}
|
|
632
|
-
if (spec.id === "deepseek-v4-flash" && item.supported_endpoint_types.includes("anthropic")) {
|
|
633
|
-
claudeModels.push({
|
|
634
|
-
...modelObj,
|
|
635
|
-
compat: {
|
|
636
|
-
forceAdaptiveThinking: true,
|
|
637
|
-
allowEmptySignature: true,
|
|
638
|
-
sendSessionAffinityHeaders: true,
|
|
639
|
-
supportsEagerToolInputStreaming: false,
|
|
640
|
-
},
|
|
641
|
-
});
|
|
642
|
-
}
|
|
643
1225
|
} else {
|
|
644
1226
|
newModels.push(id);
|
|
645
1227
|
const isAnthropic =
|
|
@@ -688,17 +1270,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
688
1270
|
} else {
|
|
689
1271
|
openaiModels.push(modelObj);
|
|
690
1272
|
}
|
|
691
|
-
if (spec.id === "deepseek-v4-flash") {
|
|
692
|
-
claudeModels.push({
|
|
693
|
-
...modelObj,
|
|
694
|
-
compat: {
|
|
695
|
-
forceAdaptiveThinking: true,
|
|
696
|
-
allowEmptySignature: true,
|
|
697
|
-
sendSessionAffinityHeaders: true,
|
|
698
|
-
supportsEagerToolInputStreaming: false,
|
|
699
|
-
},
|
|
700
|
-
});
|
|
701
|
-
}
|
|
702
1273
|
}
|
|
703
1274
|
}
|
|
704
1275
|
|
|
@@ -736,6 +1307,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
736
1307
|
return newModels;
|
|
737
1308
|
}
|
|
738
1309
|
|
|
1310
|
+
cleanupDeepSeekDuplicates();
|
|
739
1311
|
const cachedPricing = loadCachedPricing();
|
|
740
1312
|
registerAgentRouterProviders(currentApiKey, cachedPricing);
|
|
741
1313
|
|
|
@@ -762,13 +1334,26 @@ export default function (pi: ExtensionAPI) {
|
|
|
762
1334
|
|
|
763
1335
|
const payload = event.payload;
|
|
764
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);
|
|
1340
|
+
const isDeepSeek = isDeepSeekRequest(event, ctx);
|
|
1341
|
+
|
|
765
1342
|
if (payload.system !== undefined) {
|
|
766
1343
|
payload.system = enforceCanonicalRootPrompt(payload.system);
|
|
767
1344
|
}
|
|
768
|
-
if (Array.isArray(payload.messages) && payload.messages.length > 0) {
|
|
769
|
-
normalizeMessagesForAgentRouter(payload.messages);
|
|
770
1345
|
|
|
771
|
-
|
|
1346
|
+
applyPoisonRedaction(payload);
|
|
1347
|
+
|
|
1348
|
+
const messages = Array.isArray(payload.messages) ? payload.messages : payload.input;
|
|
1349
|
+
if (Array.isArray(messages) && messages.length > 0) {
|
|
1350
|
+
normalizeMessagesForAgentRouter(messages, isDeepSeek);
|
|
1351
|
+
|
|
1352
|
+
if (isDeepSeek) {
|
|
1353
|
+
frameUserTurnsForDeepSeek(messages);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
const firstMsg = messages[0];
|
|
772
1357
|
if (firstMsg && (firstMsg.role === "system" || firstMsg.role === "developer")) {
|
|
773
1358
|
firstMsg.role = "system";
|
|
774
1359
|
if (typeof firstMsg.content === "string") {
|
|
@@ -787,24 +1372,70 @@ export default function (pi: ExtensionAPI) {
|
|
|
787
1372
|
});
|
|
788
1373
|
|
|
789
1374
|
pi.on("message_end", async (event, ctx) => {
|
|
790
|
-
const
|
|
791
|
-
if (
|
|
1375
|
+
const message = (event as any)?.message;
|
|
1376
|
+
if (!message) return;
|
|
1377
|
+
|
|
1378
|
+
if (message.role === "assistant" && message.stopReason !== "error") {
|
|
1379
|
+
wafNotified = false;
|
|
1380
|
+
isSensitiveBlock = false;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
const provider = message.provider ?? ctx?.model?.provider;
|
|
1384
|
+
const baseUrl = (message as any)?.baseUrl ?? (ctx?.model as any)?.baseUrl;
|
|
1385
|
+
if (isAgentRouter(provider, baseUrl)) {
|
|
792
1386
|
setLastRequestEndTime(Date.now());
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
if (message.role !== "assistant" || message.stopReason !== "error") return;
|
|
1390
|
+
if (!isAgentRouter(provider, baseUrl)) return;
|
|
1391
|
+
|
|
1392
|
+
const errorMessage = message.errorMessage ?? "";
|
|
1393
|
+
if (!WAF_BLOCK_RE.test(errorMessage)) return;
|
|
1394
|
+
|
|
1395
|
+
escalatePending = true;
|
|
1396
|
+
if (SENSITIVE_WORDS_RE.test(errorMessage)) {
|
|
1397
|
+
isSensitiveBlock = true;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
if (exhausted) {
|
|
1401
|
+
if (!wafNotified) {
|
|
1402
|
+
wafNotified = true;
|
|
1403
|
+
const warning =
|
|
1404
|
+
"AgentRouter content filter keeps blocking even with earlier messages hidden. " +
|
|
1405
|
+
"Your latest message is likely the trigger — please rephrase or split it.";
|
|
1406
|
+
if (ctx?.hasUI) {
|
|
1407
|
+
ctx.ui.notify(warning, "warning");
|
|
1408
|
+
} else {
|
|
1409
|
+
console.warn(`\n⚠️ ${warning}\n`);
|
|
1410
|
+
}
|
|
796
1411
|
}
|
|
1412
|
+
return;
|
|
797
1413
|
}
|
|
1414
|
+
|
|
1415
|
+
if (!wafNotified) {
|
|
1416
|
+
wafNotified = true;
|
|
1417
|
+
const note = isSensitiveBlock
|
|
1418
|
+
? "Sensitive words detected in agent activity. Neutralizing previous leftovers so subsequent chats can proceed safely."
|
|
1419
|
+
: "AgentRouter content filter blocked the request. Retrying automatically with earlier messages hidden.";
|
|
1420
|
+
if (ctx?.hasUI) {
|
|
1421
|
+
ctx.ui.notify(note, "warning");
|
|
1422
|
+
} else {
|
|
1423
|
+
console.warn(`\n⚠️ ${note}\n`);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
return {
|
|
1428
|
+
message: {
|
|
1429
|
+
...message,
|
|
1430
|
+
errorMessage: `${errorMessage} (provider returned error — retrying with earlier messages hidden)`,
|
|
1431
|
+
},
|
|
1432
|
+
};
|
|
798
1433
|
});
|
|
799
1434
|
|
|
800
|
-
pi.on("turn_end", async (
|
|
1435
|
+
pi.on("turn_end", async (_event, ctx) => {
|
|
801
1436
|
const model = ctx?.model;
|
|
802
1437
|
if (isAgentRouter(model?.provider, (model as any)?.baseUrl)) {
|
|
803
1438
|
setLastRequestEndTime(Date.now());
|
|
804
|
-
const msg = (event as any)?.message;
|
|
805
|
-
if (msg && msg.role === "assistant" && msg.stopReason === "error") {
|
|
806
|
-
checkAndNotifyContentBlocked(msg.errorMessage, ctx);
|
|
807
|
-
}
|
|
808
1439
|
}
|
|
809
1440
|
});
|
|
810
1441
|
|
|
@@ -817,6 +1448,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
817
1448
|
|
|
818
1449
|
pi.on("session_start", async (_event, ctx) => {
|
|
819
1450
|
currentApiKey = getEffectiveApiKey();
|
|
1451
|
+
resetPoisonRedactionState();
|
|
1452
|
+
cleanupDeepSeekDuplicates();
|
|
820
1453
|
updatePromptRewriteEnvForModel(ctx.model);
|
|
821
1454
|
setLastRequestEndTime(Date.now());
|
|
822
1455
|
syncEnabledModelsInSettings();
|
|
@@ -825,6 +1458,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
825
1458
|
if (livePricing) {
|
|
826
1459
|
saveCachedPricing(livePricing);
|
|
827
1460
|
const newModels = registerAgentRouterProviders(currentApiKey, livePricing);
|
|
1461
|
+
cleanupDeepSeekDuplicates();
|
|
828
1462
|
syncEnabledModelsInSettings();
|
|
829
1463
|
if (newModels.length > 0 && ctx.hasUI) {
|
|
830
1464
|
ctx.ui.notify(
|
|
@@ -1133,7 +1767,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1133
1767
|
}
|
|
1134
1768
|
|
|
1135
1769
|
ctx.ui.notify(
|
|
1136
|
-
`[AgentRouter Plugin v2.1.
|
|
1770
|
+
`[AgentRouter Plugin v2.1.3]\n` +
|
|
1137
1771
|
`- Active model: ${activeModel?.id || "none"} (${isAR ? "AgentRouter [yes]" : "Other Provider"})\n` +
|
|
1138
1772
|
`- Package Priority: ${priorityStatus}\n` +
|
|
1139
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"
|