@madgagarin/pi-agentrouter 2.1.1 → 2.1.2
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 +9 -10
- package/index.ts +578 -45
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,15 +36,14 @@ 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
40
|
- **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
41
|
- **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`.
|
|
42
|
+
- **WAF Diagnostics & Safe Redaction:** Intercepts upstream blocks and safely redacts older messages while preserving thinking placeholders for reasoning models.
|
|
44
43
|
- **Isolated Credential Storage:** Manages API keys exclusively within `agentrouter-*` provider namespaces in `auth.json` without modifying default third-party provider keys.
|
|
45
44
|
- **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
45
|
- **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
|
|
46
|
+
- **Prompt Caching Compatibility:** Preserves affinity headers and pricing for prompt caching reuse.
|
|
48
47
|
|
|
49
48
|
---
|
|
50
49
|
|
|
@@ -52,12 +51,12 @@ pi install npm:@madgagarin/pi-agentrouter
|
|
|
52
51
|
|
|
53
52
|
Rates are fetched from the [agentrouter.org](https://agentrouter.org) gateway API ($2.00 / 1M tokens base unit):
|
|
54
53
|
|
|
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 |
|
|
54
|
+
| Model | Provider | Context | Output | Reasoning | Input / 1M | Output / 1M | Cache Read / 1M | Quota Policy |
|
|
55
|
+
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
|
|
56
|
+
| `deepseek-v4-flash` | `agentrouter-openai` | 1M | 64K | Yes | $4.00 | $12.00 | $2.00 | Unlimited |
|
|
57
|
+
| `glm-5.3` | `agentrouter-openai` | 1M | 128K | Yes | $3.00 | $12.00 | - | Unlimited |
|
|
58
|
+
| `gpt-6-astra` | `agentrouter-openai` | 1M | 128K | Yes | $3.00 | $15.00 | - | Daily batch drops |
|
|
59
|
+
| `gpt-5.6-sol` | `agentrouter-openai` | 1M | 128K | Yes | $3.00 | $15.00 | - | Daily batch drops |
|
|
61
60
|
| `claude-opus-5` | `agentrouter-clode` | 1M | 64K | Yes (Adaptive) | $6.00 | $30.00 | Daily batch drops |
|
|
62
61
|
| `claude-opus-4-8` | `agentrouter-clode` | 1M | 64K | Yes (Adaptive) | $8.00 | $40.00 | Daily batch drops |
|
|
63
62
|
|
package/index.ts
CHANGED
|
@@ -59,7 +59,7 @@ export const KNOWN_MODEL_SPECS: Record<string, ModelSpec> = {
|
|
|
59
59
|
maxTokens: 65536,
|
|
60
60
|
reasoning: true,
|
|
61
61
|
compat: { sendSessionAffinityHeaders: true },
|
|
62
|
-
cost: { input: 4.0 / 1_000_000, output: 12.0 / 1_000_000, cacheRead: 0, cacheWrite: 0 },
|
|
62
|
+
cost: { input: 4.0 / 1_000_000, output: 12.0 / 1_000_000, cacheRead: 2.0 / 1_000_000, cacheWrite: 0 },
|
|
63
63
|
},
|
|
64
64
|
"deepseek-v4f": {
|
|
65
65
|
id: "deepseek-v4f",
|
|
@@ -69,7 +69,7 @@ export const KNOWN_MODEL_SPECS: Record<string, ModelSpec> = {
|
|
|
69
69
|
maxTokens: 65536,
|
|
70
70
|
reasoning: true,
|
|
71
71
|
compat: { sendSessionAffinityHeaders: true },
|
|
72
|
-
cost: { input: 4.0 / 1_000_000, output: 12.0 / 1_000_000, cacheRead: 0, cacheWrite: 0 },
|
|
72
|
+
cost: { input: 4.0 / 1_000_000, output: 12.0 / 1_000_000, cacheRead: 2.0 / 1_000_000, cacheWrite: 0 },
|
|
73
73
|
},
|
|
74
74
|
"glm-5.3": {
|
|
75
75
|
id: "glm-5.3",
|
|
@@ -377,6 +377,19 @@ export function enforceCanonicalRootPrompt(systemPrompt: string | any[] | undefi
|
|
|
377
377
|
return systemPrompt;
|
|
378
378
|
}
|
|
379
379
|
|
|
380
|
+
export function isDeepSeekRequest(event: any, ctx: any): boolean {
|
|
381
|
+
const modelId = (
|
|
382
|
+
(event as any)?.model?.id ||
|
|
383
|
+
(ctx as any)?.model?.id ||
|
|
384
|
+
(event as any)?.payload?.model ||
|
|
385
|
+
""
|
|
386
|
+
).toLowerCase();
|
|
387
|
+
return modelId.includes("deepseek");
|
|
388
|
+
}
|
|
389
|
+
export const WAF_BLOCK_RE = /sensitive[_ ]words?[_ ]detected|content-blocked/i;
|
|
390
|
+
export const SENSITIVE_WORDS_RE = /sensitive[_ ]words?[_ ]detected/i;
|
|
391
|
+
export const REDACTED_NOTE = "[Message withheld by local policy]";
|
|
392
|
+
|
|
380
393
|
export function cleanJsonSchemaObject(schema: any): void {
|
|
381
394
|
if (!schema || typeof schema !== "object") return;
|
|
382
395
|
|
|
@@ -414,8 +427,351 @@ export function sanitizeOpenAiTools(tools: any[]): void {
|
|
|
414
427
|
}
|
|
415
428
|
}
|
|
416
429
|
|
|
417
|
-
export function
|
|
430
|
+
export function isRecord(v: unknown): v is Record<string, unknown> {
|
|
431
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export const redactSet = new Set<string>();
|
|
435
|
+
export let escalatePending = false;
|
|
436
|
+
export let isSensitiveBlock = false;
|
|
437
|
+
export let exhausted = false;
|
|
438
|
+
export let sessionAnchor: string | null = null;
|
|
439
|
+
export let wafNotified = false;
|
|
440
|
+
|
|
441
|
+
export function resetPoisonRedactionState(): void {
|
|
442
|
+
redactSet.clear();
|
|
443
|
+
escalatePending = false;
|
|
444
|
+
isSensitiveBlock = false;
|
|
445
|
+
exhausted = false;
|
|
446
|
+
sessionAnchor = null;
|
|
447
|
+
wafNotified = false;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export function triggerEscalation(sensitive: boolean = false): void {
|
|
451
|
+
escalatePending = true;
|
|
452
|
+
isSensitiveBlock = sensitive;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
export function fingerprintOf(msg: Record<string, unknown>): string {
|
|
457
|
+
const tc = Array.isArray(msg.tool_calls) ? JSON.stringify(msg.tool_calls).slice(0, 80) : "";
|
|
458
|
+
if (msg.type === "function_call" || msg.type === "function_call_output") {
|
|
459
|
+
return `${msg.type}:${(msg as any).call_id}:${JSON.stringify((msg as any).arguments ?? (msg as any).output ?? "").slice(0, 160)}`;
|
|
460
|
+
}
|
|
461
|
+
return `${msg.role ?? msg.type}:${JSON.stringify(msg.content ?? "").slice(0, 160)}:${tc}`;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export function firstUserAnchor(messages: unknown[]): string {
|
|
465
|
+
for (const m of messages) {
|
|
466
|
+
if (isRecord(m) && isHumanUserMessage(m as Record<string, unknown>)) return fingerprintOf(m);
|
|
467
|
+
}
|
|
468
|
+
return String(messages.length);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export function isTextBlock(block: Record<string, unknown>): boolean {
|
|
472
|
+
return block.type === "text" || block.type === "input_text" || block.type === "output_text";
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export function isHumanUserMessage(msg: Record<string, unknown>): boolean {
|
|
476
|
+
if (msg.role !== "user") return false;
|
|
477
|
+
if (typeof msg.content === "string") return true;
|
|
478
|
+
if (Array.isArray(msg.content)) {
|
|
479
|
+
const hasToolResult = msg.content.some((b) => isRecord(b) && b.type === "tool_result");
|
|
480
|
+
const hasHumanText = msg.content.some(
|
|
481
|
+
(b) =>
|
|
482
|
+
isRecord(b) &&
|
|
483
|
+
(b.type === "text" || b.type === "input_text") &&
|
|
484
|
+
typeof b.text === "string" &&
|
|
485
|
+
b.text.trim().length > 0 &&
|
|
486
|
+
b.text !== REDACTED_NOTE
|
|
487
|
+
);
|
|
488
|
+
if (hasToolResult && !hasHumanText) return false;
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export function lastHumanUserIndex(messages: unknown[]): number {
|
|
495
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
496
|
+
if (isRecord(messages[i]) && isHumanUserMessage(messages[i] as Record<string, unknown>)) {
|
|
497
|
+
return i;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return -1;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export function hasRedactableText(content: unknown, msg?: Record<string, unknown>): boolean {
|
|
504
|
+
if (msg) {
|
|
505
|
+
if (msg.details && (typeof msg.details === "string" || Object.keys(msg.details).length > 0)) return true;
|
|
506
|
+
if (typeof (msg as any).reasoning_content === "string" && (msg as any).reasoning_content.length > 0) return true;
|
|
507
|
+
if (typeof (msg as any).thinking === "string" && (msg as any).thinking.length > 0) return true;
|
|
508
|
+
}
|
|
509
|
+
if (msg?.type === "function_call") return typeof (msg as any).arguments === "string" && (msg as any).arguments !== "{}";
|
|
510
|
+
if (msg?.type === "function_call_output") return hasRedactableText((msg as any).output);
|
|
511
|
+
if (typeof content === "string") return content.length > 0 && content !== REDACTED_NOTE;
|
|
512
|
+
if (Array.isArray(content)) {
|
|
513
|
+
for (const b of content) {
|
|
514
|
+
if (!isRecord(b)) continue;
|
|
515
|
+
if (b.type === "thinking" || b.type === "reasoning") return true;
|
|
516
|
+
if (isTextBlock(b) && typeof b.text === "string" && b.text.length > 0 && b.text !== REDACTED_NOTE) return true;
|
|
517
|
+
if (b.type === "tool_result") {
|
|
518
|
+
if (typeof b.content === "string" && b.content.length > 0 && b.content !== REDACTED_NOTE) return true;
|
|
519
|
+
if (Array.isArray(b.content)) {
|
|
520
|
+
for (const c of b.content) {
|
|
521
|
+
if (isRecord(c) && isTextBlock(c) && typeof c.text === "string" && c.text.length > 0 && c.text !== REDACTED_NOTE) {
|
|
522
|
+
return true;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (b.type === "tool_use" && isRecord(b.input) && Object.keys(b.input).length > 0) {
|
|
528
|
+
return true;
|
|
529
|
+
}
|
|
530
|
+
if (b.type === "toolCall" && isRecord(b.arguments) && Object.keys(b.arguments).length > 0) {
|
|
531
|
+
return true;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (msg && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
|
|
536
|
+
for (const tc of msg.tool_calls) {
|
|
537
|
+
if (isRecord(tc) && isRecord(tc.function) && typeof tc.function.arguments === "string" && tc.function.arguments !== "{}") {
|
|
538
|
+
return true;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
export function redactBlocks(content: unknown): void {
|
|
546
|
+
if (!Array.isArray(content)) return;
|
|
547
|
+
for (let idx = content.length - 1; idx >= 0; idx--) {
|
|
548
|
+
const block = content[idx];
|
|
549
|
+
if (!isRecord(block)) continue;
|
|
550
|
+
|
|
551
|
+
// For thinking / reasoning blocks, keep a harmless placeholder rather than deleting,
|
|
552
|
+
// because thinking models (like DeepSeek) strictly require thinking blocks to be present in multi-turn history
|
|
553
|
+
if (block.type === "thinking" || block.type === "reasoning") {
|
|
554
|
+
if (typeof (block as any).thinking === "string") {
|
|
555
|
+
(block as any).thinking = "Thinking...";
|
|
556
|
+
}
|
|
557
|
+
if (typeof (block as any).text === "string") {
|
|
558
|
+
(block as any).text = "Thinking...";
|
|
559
|
+
}
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (isTextBlock(block) && typeof block.text === "string") {
|
|
564
|
+
block.text = REDACTED_NOTE;
|
|
565
|
+
} else if (block.type === "tool_result") {
|
|
566
|
+
if (typeof block.content === "string") {
|
|
567
|
+
block.content = REDACTED_NOTE;
|
|
568
|
+
} else if (Array.isArray(block.content)) {
|
|
569
|
+
for (const b of block.content) {
|
|
570
|
+
if (isRecord(b) && isTextBlock(b) && typeof b.text === "string") {
|
|
571
|
+
b.text = REDACTED_NOTE;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
} else if (block.type === "tool_use") {
|
|
576
|
+
block.input = {};
|
|
577
|
+
} else if (block.type === "toolCall") {
|
|
578
|
+
block.arguments = {};
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
export function isHideable(msg: Record<string, unknown>): boolean {
|
|
584
|
+
return (
|
|
585
|
+
msg.role === "user" ||
|
|
586
|
+
msg.role === "assistant" ||
|
|
587
|
+
msg.role === "tool" ||
|
|
588
|
+
msg.role === "toolResult" ||
|
|
589
|
+
msg.type === "function_call" ||
|
|
590
|
+
msg.type === "function_call_output"
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
export function redactMessageAt(messages: unknown[], i: number): void {
|
|
595
|
+
const msg = messages[i];
|
|
596
|
+
if (!isRecord(msg)) return;
|
|
597
|
+
|
|
598
|
+
// Clear tool result details / raw diffs / attachments
|
|
599
|
+
if ("details" in msg) delete msg.details;
|
|
600
|
+
// DeepSeek and reasoning models require reasoning_content/thinking to exist in thinking mode.
|
|
601
|
+
// Never delete them completely; keep a sanitized minimal placeholder.
|
|
602
|
+
if ("reasoning_content" in msg) {
|
|
603
|
+
(msg as any).reasoning_content = "Thinking...";
|
|
604
|
+
}
|
|
605
|
+
if ("thinking" in msg) {
|
|
606
|
+
(msg as any).thinking = "Thinking...";
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (msg.type === "function_call") {
|
|
610
|
+
(msg as any).arguments = "{}";
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
if (msg.type === "function_call_output") {
|
|
614
|
+
if (typeof (msg as any).output === "string") (msg as any).output = REDACTED_NOTE;
|
|
615
|
+
else redactBlocks((msg as any).output);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (typeof msg.content === "string") {
|
|
619
|
+
msg.content = REDACTED_NOTE;
|
|
620
|
+
} else if (Array.isArray(msg.content)) {
|
|
621
|
+
redactBlocks(msg.content);
|
|
622
|
+
if (msg.content.length === 0 && !Array.isArray(msg.tool_calls)) {
|
|
623
|
+
msg.content = [{ type: "text", text: REDACTED_NOTE }];
|
|
624
|
+
}
|
|
625
|
+
} else if (msg.role === "tool" || msg.role === "toolResult" || msg.role === "assistant") {
|
|
626
|
+
msg.content = REDACTED_NOTE;
|
|
627
|
+
}
|
|
628
|
+
if (Array.isArray(msg.tool_calls)) {
|
|
629
|
+
for (const tc of msg.tool_calls) {
|
|
630
|
+
if (isRecord(tc) && isRecord(tc.function) && typeof tc.function.arguments === "string") {
|
|
631
|
+
tc.function.arguments = "{}";
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
export function applyPoisonRedaction(payload: Record<string, unknown>): void {
|
|
638
|
+
const messages = Array.isArray(payload.messages) ? payload.messages : (payload as any).input;
|
|
639
|
+
if (!Array.isArray(messages) || messages.length === 0) return;
|
|
640
|
+
|
|
641
|
+
// Prune failed assistant messages carrying error status/text in-place so dead error turns do not linger
|
|
642
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
643
|
+
const m = messages[i];
|
|
644
|
+
if (!isRecord(m)) continue;
|
|
645
|
+
if (m.role === "assistant") {
|
|
646
|
+
if (m.stopReason === "error") {
|
|
647
|
+
messages.splice(i, 1);
|
|
648
|
+
} else if (typeof m.content === "string" && WAF_BLOCK_RE.test(m.content)) {
|
|
649
|
+
messages.splice(i, 1);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const fps = messages.map((m) => (isRecord(m) ? fingerprintOf(m) : ""));
|
|
655
|
+
|
|
656
|
+
const anchor = firstUserAnchor(messages);
|
|
657
|
+
if (anchor !== sessionAnchor) {
|
|
658
|
+
const firstContact = sessionAnchor === null;
|
|
659
|
+
sessionAnchor = anchor;
|
|
660
|
+
if (!firstContact) {
|
|
661
|
+
redactSet.clear();
|
|
662
|
+
escalatePending = false;
|
|
663
|
+
isSensitiveBlock = false;
|
|
664
|
+
exhausted = false;
|
|
665
|
+
wafNotified = false;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (redactSet.size > 0 && !fps.some((fp) => fp && redactSet.has(fp))) {
|
|
670
|
+
redactSet.clear();
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const lastHumanUser = lastHumanUserIndex(messages);
|
|
674
|
+
|
|
675
|
+
for (let i = 0; i < messages.length; i++) {
|
|
676
|
+
if (i === lastHumanUser) continue;
|
|
677
|
+
if (!isRecord(messages[i]) || !isHideable(messages[i] as Record<string, unknown>)) continue;
|
|
678
|
+
if (redactSet.has(fps[i])) redactMessageAt(messages, i);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (escalatePending) {
|
|
682
|
+
escalatePending = false;
|
|
683
|
+
const sensitive = isSensitiveBlock;
|
|
684
|
+
isSensitiveBlock = false;
|
|
685
|
+
|
|
686
|
+
if (sensitive) {
|
|
687
|
+
// Sensitive words detected: neutralize ALL turns (older user, tool, assistant, active tool results) except active human user
|
|
688
|
+
for (let i = 0; i < messages.length; i++) {
|
|
689
|
+
if (i === lastHumanUser) continue;
|
|
690
|
+
if (!isRecord(messages[i])) continue;
|
|
691
|
+
const m = messages[i] as Record<string, unknown>;
|
|
692
|
+
if (!isHideable(m)) continue;
|
|
693
|
+
redactSet.add(fps[i]);
|
|
694
|
+
if (hasRedactableText(m.content, m)) {
|
|
695
|
+
redactMessageAt(messages, i);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
} else {
|
|
699
|
+
// Language ratio block: Stage 1 older user turns, Stage 2 assistant & tool
|
|
700
|
+
let anyRedacted = false;
|
|
701
|
+
for (let i = 0; i < messages.length; i++) {
|
|
702
|
+
if (i === lastHumanUser) continue;
|
|
703
|
+
if (!isRecord(messages[i])) continue;
|
|
704
|
+
const m = messages[i] as Record<string, unknown>;
|
|
705
|
+
if (m.role !== "user") continue;
|
|
706
|
+
if (redactSet.has(fps[i])) continue;
|
|
707
|
+
redactSet.add(fps[i]);
|
|
708
|
+
if (hasRedactableText(m.content, m)) {
|
|
709
|
+
redactMessageAt(messages, i);
|
|
710
|
+
anyRedacted = true;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
if (!anyRedacted) {
|
|
714
|
+
for (let i = 0; i < messages.length; i++) {
|
|
715
|
+
if (i === lastHumanUser) continue;
|
|
716
|
+
if (!isRecord(messages[i])) continue;
|
|
717
|
+
const m = messages[i] as Record<string, unknown>;
|
|
718
|
+
if (!isHideable(m) || m.role === "user") continue;
|
|
719
|
+
if (redactSet.has(fps[i])) continue;
|
|
720
|
+
redactSet.add(fps[i]);
|
|
721
|
+
if (hasRedactableText(m.content, m)) {
|
|
722
|
+
redactMessageAt(messages, i);
|
|
723
|
+
anyRedacted = true;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Exhausted if there are no more hideable messages left to redact other than lastHumanUser
|
|
731
|
+
exhausted = true;
|
|
732
|
+
for (let i = 0; i < messages.length; i++) {
|
|
733
|
+
if (i === lastHumanUser) continue;
|
|
734
|
+
if (!isRecord(messages[i]) || !isHideable(messages[i] as Record<string, unknown>)) continue;
|
|
735
|
+
if (!redactSet.has(fps[i])) {
|
|
736
|
+
exhausted = false;
|
|
737
|
+
break;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
export function normalizeMessagesForAgentRouter(messages: any[], isDeepSeek: boolean = false): void {
|
|
418
743
|
if (!Array.isArray(messages)) return;
|
|
744
|
+
|
|
745
|
+
// Prune poisoned/failed assistant turns globally across all models (e.g. prior 402 quota failure, network errors, empty turns)
|
|
746
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
747
|
+
const msg = messages[i];
|
|
748
|
+
if (!msg || typeof msg !== "object") continue;
|
|
749
|
+
if (msg.role === "assistant") {
|
|
750
|
+
const hasToolCalls = Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0;
|
|
751
|
+
const hasToolUseBlocks =
|
|
752
|
+
Array.isArray(msg.content) &&
|
|
753
|
+
msg.content.some((b: any) => b && typeof b === "object" && (b.type === "tool_use" || b.type === "tool_call"));
|
|
754
|
+
const hasTools = hasToolCalls || hasToolUseBlocks;
|
|
755
|
+
const isEmptyContent =
|
|
756
|
+
!msg.content ||
|
|
757
|
+
msg.content === "" ||
|
|
758
|
+
(Array.isArray(msg.content) && (
|
|
759
|
+
msg.content.length === 0 ||
|
|
760
|
+
msg.content.every((b: any) => b && typeof b === "object" && (b.type === "text" || b.type === "input_text") && (!b.text || b.text.trim() === ""))
|
|
761
|
+
));
|
|
762
|
+
|
|
763
|
+
if (
|
|
764
|
+
msg.stopReason === "error" ||
|
|
765
|
+
(msg.stopReason === "aborted" && isEmptyContent) ||
|
|
766
|
+
(typeof msg.content === "string" && WAF_BLOCK_RE.test(msg.content)) ||
|
|
767
|
+
(isEmptyContent && !hasTools && !msg.reasoning_content)
|
|
768
|
+
) {
|
|
769
|
+
messages.splice(i, 1);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
|
|
419
775
|
for (const msg of messages) {
|
|
420
776
|
if (!msg || typeof msg !== "object") continue;
|
|
421
777
|
if (msg.role === "developer") {
|
|
@@ -455,13 +811,157 @@ export function normalizeMessagesForAgentRouter(messages: any[]): void {
|
|
|
455
811
|
}
|
|
456
812
|
|
|
457
813
|
// If assistant executed tool calls, AgentRouter DeepSeek proxy strictly requires reasoning_content
|
|
458
|
-
if (
|
|
814
|
+
if (
|
|
815
|
+
Array.isArray(msg.tool_calls) &&
|
|
816
|
+
msg.tool_calls.length > 0 &&
|
|
817
|
+
(!msg.reasoning_content || (typeof msg.reasoning_content === "string" && !msg.reasoning_content.trim()))
|
|
818
|
+
) {
|
|
459
819
|
msg.reasoning_content = "Executing tools...";
|
|
460
820
|
}
|
|
821
|
+
|
|
822
|
+
// If DeepSeek model and content is still empty without tool calls, provide fallback text
|
|
823
|
+
if (isDeepSeek && (!msg.content || msg.content === "") && (!Array.isArray(msg.tool_calls) || msg.tool_calls.length === 0)) {
|
|
824
|
+
msg.content = "[Interrupted]";
|
|
825
|
+
if (!msg.reasoning_content) {
|
|
826
|
+
msg.reasoning_content = "Interrupted response";
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (isDeepSeek) {
|
|
832
|
+
// Flatten past assistant tool_calls into text and convert tool roles into user turns.
|
|
833
|
+
// This prevents AgentRouter's upstream Anthropic gateway from rejecting the request with:
|
|
834
|
+
// "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
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
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;
|
|
856
|
+
}
|
|
857
|
+
|
|
461
858
|
}
|
|
462
859
|
}
|
|
463
860
|
}
|
|
464
861
|
|
|
862
|
+
export function cleanupDeepSeekDuplicates(): {
|
|
863
|
+
cleanedSettings: boolean;
|
|
864
|
+
cleanedCache: boolean;
|
|
865
|
+
cleanedModelsJson: boolean;
|
|
866
|
+
} {
|
|
867
|
+
let cleanedSettings = false;
|
|
868
|
+
let cleanedCache = false;
|
|
869
|
+
let cleanedModelsJson = false;
|
|
870
|
+
|
|
871
|
+
// 1. Clean settings.json
|
|
872
|
+
try {
|
|
873
|
+
if (fs.existsSync(SETTINGS_FILE)) {
|
|
874
|
+
const settings = JSON.parse(fs.readFileSync(SETTINGS_FILE, "utf-8"));
|
|
875
|
+
let modified = false;
|
|
876
|
+
|
|
877
|
+
if (Array.isArray(settings.enabledModels)) {
|
|
878
|
+
const clodeIdx = settings.enabledModels.indexOf("agentrouter-clode/deepseek-v4-flash");
|
|
879
|
+
if (clodeIdx !== -1) {
|
|
880
|
+
settings.enabledModels.splice(clodeIdx, 1);
|
|
881
|
+
modified = true;
|
|
882
|
+
}
|
|
883
|
+
// Deduplicate enabledModels
|
|
884
|
+
const seen = new Set<string>();
|
|
885
|
+
const deduped: string[] = [];
|
|
886
|
+
for (const m of settings.enabledModels) {
|
|
887
|
+
if (!seen.has(m)) {
|
|
888
|
+
seen.add(m);
|
|
889
|
+
deduped.push(m);
|
|
890
|
+
} else {
|
|
891
|
+
modified = true;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
settings.enabledModels = deduped;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
if (settings.subagents && typeof settings.subagents === "object" && settings.subagents.agentOverrides) {
|
|
898
|
+
for (const override of Object.values(settings.subagents.agentOverrides)) {
|
|
899
|
+
if ((override as any)?.model === "agentrouter-clode/deepseek-v4-flash") {
|
|
900
|
+
(override as any).model = "agentrouter-openai/deepseek-v4-flash";
|
|
901
|
+
modified = true;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
if (settings.defaultProvider === "agentrouter-clode" && settings.defaultModel === "deepseek-v4-flash") {
|
|
907
|
+
settings.defaultProvider = "agentrouter-openai";
|
|
908
|
+
modified = true;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
if (modified) {
|
|
912
|
+
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), "utf-8");
|
|
913
|
+
cleanedSettings = true;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
} catch {}
|
|
917
|
+
|
|
918
|
+
// 2. Clean MODELS_CACHE_FILE (.agentrouter-models-cache.json)
|
|
919
|
+
try {
|
|
920
|
+
if (fs.existsSync(MODELS_CACHE_FILE)) {
|
|
921
|
+
const cacheData = JSON.parse(fs.readFileSync(MODELS_CACHE_FILE, "utf-8"));
|
|
922
|
+
if (Array.isArray(cacheData)) {
|
|
923
|
+
let modified = false;
|
|
924
|
+
for (const item of cacheData) {
|
|
925
|
+
if (item && item.model_name === "deepseek-v4-flash") {
|
|
926
|
+
if (Array.isArray(item.supported_endpoint_types) && item.supported_endpoint_types.includes("anthropic")) {
|
|
927
|
+
item.supported_endpoint_types = item.supported_endpoint_types.filter((t: string) => t !== "anthropic");
|
|
928
|
+
modified = true;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
if (modified) {
|
|
933
|
+
fs.writeFileSync(MODELS_CACHE_FILE, JSON.stringify(cacheData, null, 2), "utf-8");
|
|
934
|
+
cleanedCache = true;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
} catch {}
|
|
939
|
+
|
|
940
|
+
// 3. Clean models.json (if present)
|
|
941
|
+
try {
|
|
942
|
+
const modelsJsonPath = path.join(process.env.HOME || "", ".pi/agent/models.json");
|
|
943
|
+
if (fs.existsSync(modelsJsonPath)) {
|
|
944
|
+
const modelsJson = JSON.parse(fs.readFileSync(modelsJsonPath, "utf-8"));
|
|
945
|
+
let modified = false;
|
|
946
|
+
if (modelsJson?.providers?.["agentrouter-clode"]?.models) {
|
|
947
|
+
const origLen = modelsJson.providers["agentrouter-clode"].models.length;
|
|
948
|
+
modelsJson.providers["agentrouter-clode"].models = modelsJson.providers["agentrouter-clode"].models.filter(
|
|
949
|
+
(m: any) => (typeof m === "string" ? m !== "deepseek-v4-flash" : m?.id !== "deepseek-v4-flash")
|
|
950
|
+
);
|
|
951
|
+
if (modelsJson.providers["agentrouter-clode"].models.length !== origLen) {
|
|
952
|
+
modified = true;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
if (modified) {
|
|
956
|
+
fs.writeFileSync(modelsJsonPath, JSON.stringify(modelsJson, null, 2), "utf-8");
|
|
957
|
+
cleanedModelsJson = true;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
} catch {}
|
|
961
|
+
|
|
962
|
+
return { cleanedSettings, cleanedCache, cleanedModelsJson };
|
|
963
|
+
}
|
|
964
|
+
|
|
465
965
|
let lastWarnedErrorTimestamp = 0;
|
|
466
966
|
export function checkAndNotifyContentBlocked(errMessage: string | undefined, ctx: any): void {
|
|
467
967
|
if (!errMessage) return;
|
|
@@ -481,7 +981,7 @@ export function checkAndNotifyContentBlocked(errMessage: string | undefined, ctx
|
|
|
481
981
|
export async function fetchLivePricing(): Promise<ApiPricingModel[] | null> {
|
|
482
982
|
try {
|
|
483
983
|
const res = await fetch("https://agentrouter.org/api/pricing", {
|
|
484
|
-
|
|
984
|
+
|
|
485
985
|
});
|
|
486
986
|
if (!res.ok) return null;
|
|
487
987
|
const data = await res.json();
|
|
@@ -505,8 +1005,7 @@ export async function fetchTokenUsage(apiKey: string): Promise<number | null> {
|
|
|
505
1005
|
{
|
|
506
1006
|
headers: {
|
|
507
1007
|
Authorization: `Bearer ${apiKey}`,
|
|
508
|
-
|
|
509
|
-
},
|
|
1008
|
+
},
|
|
510
1009
|
}
|
|
511
1010
|
);
|
|
512
1011
|
if (!res.ok) return null;
|
|
@@ -532,12 +1031,12 @@ export async function probeModelQuota(modelId: string, apiKey: string, isAnthrop
|
|
|
532
1031
|
"Content-Type": "application/json",
|
|
533
1032
|
"x-api-key": apiKey,
|
|
534
1033
|
"anthropic-version": "2023-06-01",
|
|
535
|
-
"User-Agent":
|
|
1034
|
+
"User-Agent": getPiUserAgent(),
|
|
536
1035
|
}
|
|
537
1036
|
: {
|
|
538
1037
|
"Content-Type": "application/json",
|
|
539
1038
|
Authorization: `Bearer ${apiKey}`,
|
|
540
|
-
"User-Agent":
|
|
1039
|
+
"User-Agent": getPiUserAgent(),
|
|
541
1040
|
};
|
|
542
1041
|
|
|
543
1042
|
const body = isAnthropic
|
|
@@ -629,17 +1128,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
629
1128
|
} else {
|
|
630
1129
|
openaiModels.push(modelObj);
|
|
631
1130
|
}
|
|
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
1131
|
} else {
|
|
644
1132
|
newModels.push(id);
|
|
645
1133
|
const isAnthropic =
|
|
@@ -688,17 +1176,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
688
1176
|
} else {
|
|
689
1177
|
openaiModels.push(modelObj);
|
|
690
1178
|
}
|
|
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
1179
|
}
|
|
703
1180
|
}
|
|
704
1181
|
|
|
@@ -736,6 +1213,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
736
1213
|
return newModels;
|
|
737
1214
|
}
|
|
738
1215
|
|
|
1216
|
+
cleanupDeepSeekDuplicates();
|
|
739
1217
|
const cachedPricing = loadCachedPricing();
|
|
740
1218
|
registerAgentRouterProviders(currentApiKey, cachedPricing);
|
|
741
1219
|
|
|
@@ -762,13 +1240,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
762
1240
|
|
|
763
1241
|
const payload = event.payload;
|
|
764
1242
|
if (payload) {
|
|
1243
|
+
const isDeepSeek = isDeepSeekRequest(event, ctx);
|
|
1244
|
+
|
|
765
1245
|
if (payload.system !== undefined) {
|
|
766
1246
|
payload.system = enforceCanonicalRootPrompt(payload.system);
|
|
767
1247
|
}
|
|
768
|
-
if (Array.isArray(payload.messages) && payload.messages.length > 0) {
|
|
769
|
-
normalizeMessagesForAgentRouter(payload.messages);
|
|
770
1248
|
|
|
771
|
-
|
|
1249
|
+
applyPoisonRedaction(payload);
|
|
1250
|
+
|
|
1251
|
+
const messages = Array.isArray(payload.messages) ? payload.messages : payload.input;
|
|
1252
|
+
if (Array.isArray(messages) && messages.length > 0) {
|
|
1253
|
+
normalizeMessagesForAgentRouter(messages, isDeepSeek);
|
|
1254
|
+
|
|
1255
|
+
const firstMsg = messages[0];
|
|
772
1256
|
if (firstMsg && (firstMsg.role === "system" || firstMsg.role === "developer")) {
|
|
773
1257
|
firstMsg.role = "system";
|
|
774
1258
|
if (typeof firstMsg.content === "string") {
|
|
@@ -787,24 +1271,70 @@ export default function (pi: ExtensionAPI) {
|
|
|
787
1271
|
});
|
|
788
1272
|
|
|
789
1273
|
pi.on("message_end", async (event, ctx) => {
|
|
790
|
-
const
|
|
791
|
-
if (
|
|
1274
|
+
const message = (event as any)?.message;
|
|
1275
|
+
if (!message) return;
|
|
1276
|
+
|
|
1277
|
+
if (message.role === "assistant" && message.stopReason !== "error") {
|
|
1278
|
+
wafNotified = false;
|
|
1279
|
+
isSensitiveBlock = false;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
const provider = message.provider ?? ctx?.model?.provider;
|
|
1283
|
+
const baseUrl = (message as any)?.baseUrl ?? (ctx?.model as any)?.baseUrl;
|
|
1284
|
+
if (isAgentRouter(provider, baseUrl)) {
|
|
792
1285
|
setLastRequestEndTime(Date.now());
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
if (message.role !== "assistant" || message.stopReason !== "error") return;
|
|
1289
|
+
if (!isAgentRouter(provider, baseUrl)) return;
|
|
1290
|
+
|
|
1291
|
+
const errorMessage = message.errorMessage ?? "";
|
|
1292
|
+
if (!WAF_BLOCK_RE.test(errorMessage)) return;
|
|
1293
|
+
|
|
1294
|
+
escalatePending = true;
|
|
1295
|
+
if (SENSITIVE_WORDS_RE.test(errorMessage)) {
|
|
1296
|
+
isSensitiveBlock = true;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
if (exhausted) {
|
|
1300
|
+
if (!wafNotified) {
|
|
1301
|
+
wafNotified = true;
|
|
1302
|
+
const warning =
|
|
1303
|
+
"AgentRouter content filter keeps blocking even with earlier messages hidden. " +
|
|
1304
|
+
"Your latest message is likely the trigger — please rephrase or split it.";
|
|
1305
|
+
if (ctx?.hasUI) {
|
|
1306
|
+
ctx.ui.notify(warning, "warning");
|
|
1307
|
+
} else {
|
|
1308
|
+
console.warn(`\n⚠️ ${warning}\n`);
|
|
1309
|
+
}
|
|
796
1310
|
}
|
|
1311
|
+
return;
|
|
797
1312
|
}
|
|
1313
|
+
|
|
1314
|
+
if (!wafNotified) {
|
|
1315
|
+
wafNotified = true;
|
|
1316
|
+
const note = isSensitiveBlock
|
|
1317
|
+
? "Sensitive words detected in agent activity. Neutralizing previous leftovers so subsequent chats can proceed safely."
|
|
1318
|
+
: "AgentRouter content filter blocked the request. Retrying automatically with earlier messages hidden.";
|
|
1319
|
+
if (ctx?.hasUI) {
|
|
1320
|
+
ctx.ui.notify(note, "warning");
|
|
1321
|
+
} else {
|
|
1322
|
+
console.warn(`\n⚠️ ${note}\n`);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
return {
|
|
1327
|
+
message: {
|
|
1328
|
+
...message,
|
|
1329
|
+
errorMessage: `${errorMessage} (provider returned error — retrying with earlier messages hidden)`,
|
|
1330
|
+
},
|
|
1331
|
+
};
|
|
798
1332
|
});
|
|
799
1333
|
|
|
800
|
-
pi.on("turn_end", async (
|
|
1334
|
+
pi.on("turn_end", async (_event, ctx) => {
|
|
801
1335
|
const model = ctx?.model;
|
|
802
1336
|
if (isAgentRouter(model?.provider, (model as any)?.baseUrl)) {
|
|
803
1337
|
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
1338
|
}
|
|
809
1339
|
});
|
|
810
1340
|
|
|
@@ -817,6 +1347,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
817
1347
|
|
|
818
1348
|
pi.on("session_start", async (_event, ctx) => {
|
|
819
1349
|
currentApiKey = getEffectiveApiKey();
|
|
1350
|
+
resetPoisonRedactionState();
|
|
1351
|
+
cleanupDeepSeekDuplicates();
|
|
820
1352
|
updatePromptRewriteEnvForModel(ctx.model);
|
|
821
1353
|
setLastRequestEndTime(Date.now());
|
|
822
1354
|
syncEnabledModelsInSettings();
|
|
@@ -825,6 +1357,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
825
1357
|
if (livePricing) {
|
|
826
1358
|
saveCachedPricing(livePricing);
|
|
827
1359
|
const newModels = registerAgentRouterProviders(currentApiKey, livePricing);
|
|
1360
|
+
cleanupDeepSeekDuplicates();
|
|
828
1361
|
syncEnabledModelsInSettings();
|
|
829
1362
|
if (newModels.length > 0 && ctx.hasUI) {
|
|
830
1363
|
ctx.ui.notify(
|
|
@@ -1133,7 +1666,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1133
1666
|
}
|
|
1134
1667
|
|
|
1135
1668
|
ctx.ui.notify(
|
|
1136
|
-
`[AgentRouter Plugin v2.1.
|
|
1669
|
+
`[AgentRouter Plugin v2.1.2]\n` +
|
|
1137
1670
|
`- Active model: ${activeModel?.id || "none"} (${isAR ? "AgentRouter [yes]" : "Other Provider"})\n` +
|
|
1138
1671
|
`- Package Priority: ${priorityStatus}\n` +
|
|
1139
1672
|
`- 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.2",
|
|
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"
|