@madgagarin/pi-agentrouter 1.4.0 β†’ 2.1.1

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 +81 -89
  2. package/index.ts +742 -113
  3. package/package.json +12 -4
package/README.md CHANGED
@@ -1,140 +1,132 @@
1
- # pi-agentrouter
1
+ # @madgagarin/pi-agentrouter
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@madgagarin/pi-agentrouter.svg?color=blue)](https://www.npmjs.com/package/@madgagarin/pi-agentrouter)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
5
5
  [![Pi Plugin](https://img.shields.io/badge/Pi-Extension-purple.svg)](https://pi.dev)
6
- **`pi-agentrouter`** seamlessly connects 4 powerful flagship AI coding models (**GPT-5.6 Sol**, **Claude Opus 4.8**, **Claude Opus 5**, and **DeepSeek V4F**) to your [Pi Coding Agent](https://pi.dev) using a single, unified API key from [AgentRouter](https://agentrouter.org).
6
+ [![AgentRouter Gateway](https://img.shields.io/badge/Gateway-agentrouter.org-orange.svg)](https://agentrouter.org)
7
7
 
8
- ### πŸ’‘ Why use this plugin?
9
- * πŸ”‘ **All 4 Top Models in One Place:** Instantly switch between GPT-5.6 Sol, Claude Opus 4.8, Claude Opus 5, and DeepSeek V4F without managing separate subscriptions or multiple API keys.
10
- * πŸ’° **Save up to 80% on Tokens:** Smart prompt caching and session affinity drastically cut token usage and speed up responses.
11
- * ⚑ **Zero-Config Setup:** Installs in seconds β€” all models are automatically registered with native reasoning, tool calling, and thinking support.
12
- * πŸ›‘οΈ **Built-in Stability:** Automatic cross-process pacing and authentication guards eliminate 401 and 429 errors during long coding sessions and multi-agent tasks.
8
+ Use **GPT-6 Astra**, **GPT-5.6 Sol**, **Claude Opus 5**, **Claude Opus 4.8**, and **DeepSeek V4 Flash** in your [Pi Coding Agent](https://pi.dev) using a single API key from [AgentRouter](https://agentrouter.org).
9
+
10
+ > 🎁 **Free Trial Credits:** New to AgentRouter? Get up to **$175 in free credits** (including a **+$50 bonus**) to test GPT-6 Astra, Claude Opus 5, and DeepSeek V4 β€” no credit card needed. That's enough for **millions of tokens** on DeepSeek V4!
11
+ > πŸ‘‰ **[Claim your free trial credits on AgentRouter.org β†’](https://agentrouter.org/register?aff=34dc)**
13
12
 
14
13
  ---
15
14
 
16
- ## 🎁 Free Credits & Author Referral Bonus
15
+ ## Quick Start
16
+
17
+ ### 1. Get your API key
17
18
 
18
- AgentRouter is a non-profit AI API gateway providing unified access to cutting-edge models:
19
+ Create an account on [agentrouter.org](https://agentrouter.org/register?aff=34dc) to get your free trial credits and copy your `sk-...` key from the dashboard.
19
20
 
20
- * **Increased Trial Credits:** By signing up through the referral link below, you unlock **increased trial credits (up to $175 / extra +$50 bonus)** to explore GPT-5.6 Sol, Claude Opus, and DeepSeek models.
21
- * **Support the Author:** Using this link directly supports the author and the ongoing maintenance of this open-source plugin!
21
+ ### 2. Install the extension
22
22
 
23
- πŸ‘‰ **[Sign up on AgentRouter (Referral Link with Bonus)](https://agentrouter.org/register?aff=34dc)** πŸ‘ˆ
23
+ ```bash
24
+ pi install npm:@madgagarin/pi-agentrouter
25
+ ```
24
26
 
25
- *(If you already have an account, you can obtain your API key directly from your [AgentRouter Dashboard](https://agentrouter.org/dashboard)).*
27
+ ### 3. Activate in Pi chat
28
+
29
+ ```text
30
+ /agentrouter key sk-your-agentrouter-key
31
+ ```
32
+
33
+ *(Or set `export AGENTROUTER_API_KEY="sk-..."` in your shell).*
26
34
 
27
35
  ---
28
36
 
29
- ## ✨ Features
30
-
31
- - πŸ”‘ **Unified API Key**: Use a single `sk-...` (or custom token) for all modelsβ€”both OpenAI GPT, DeepSeek, and Anthropic Claude endpoints are authenticated seamlessly.
32
- - πŸš€ **Zero-Config Model Auto-Registration**: Automatically registers:
33
- - `agentrouter-openai/gpt-5.6-sol` (1M Context Window, Native Reasoning, Session Affinity).
34
- - `agentrouter-openai/deepseek-v4f` (128K Context Window, Reasoning Support, Session Affinity).
35
- - `agentrouter-clode/claude-opus-4-8` (512K Context Window, Adaptive Thinking, Empty Signature Compat).
36
- - `agentrouter-clode/claude-opus-5` (1M Context Window, Adaptive Thinking, Empty Signature Compat).
37
- - πŸ›‘οΈ **Cross-Process Request Pacing**: Shared file-based rate limiter (`~/.pi/agent/.agentrouter-pacing`) synchronizing delays across concurrent subagents, background workers, and the main Pi process to prevent WAF burst blocks.
38
- - πŸ”’ **Transport-Level Root Prompt Guard**: Enforces canonical `pi-code` harness header at `index: 0` on every outbound request (`before_provider_request`), preventing 401 unauthorized client errors across multi-turn tool executions.
39
- - ⚑ **Optimized Prompt Caching & Dynamic Bypass**: Automatically disables aggressive prompt rewriting on AgentRouter routes (`PI_CACHE_OPTIMIZER_NO_PROMPT_REWRITE=1`) while preserving session affinity headers, achieving **>80% cache hit rates** on consecutive turns.
40
- - πŸ“‹ **Package Priority Guard & Auto-Fix**: Automatically checks package order in `settings.json` on startup and offers interactive one-click placement directly above `pi-cache-optimizer`.
41
- - πŸ“¦ **Seamless Compaction (Fixes 401)**: Solves the `401 unauthorized client` error during `/compact` by ensuring valid Pi client fingerprint headers are passed during summarization.
42
- - ⌨️ **Interactive Terminal Controls**: Change API keys, adjust throttle pacing, fix package priority, and cycle through models or thinking depths directly in the TUI.
37
+ ## Features
38
+
39
+ - **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
+ - **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 `content-blocked` responses and displays a clear notification in the terminal and UI.
43
+ - **Dual Endpoint Protocols:** Supports both OpenAI (`agentrouter-openai`) and Anthropic Messages API (`agentrouter-clode`) routes for models like `deepseek-v4-flash`.
44
+ - **Isolated Credential Storage:** Manages API keys exclusively within `agentrouter-*` provider namespaces in `auth.json` without modifying default third-party provider keys.
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
+ - **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 formatting required for upstream prompt cache reuse.
43
48
 
44
49
  ---
45
50
 
46
- ## πŸš€ Installation
51
+ ## Models & Pricing
47
52
 
48
- ### Option 1: Install via npm (Recommended)
49
- ```bash
50
- pi install npm:@madgagarin/pi-agentrouter
51
- ```
53
+ Rates are fetched from the [agentrouter.org](https://agentrouter.org) gateway API ($2.00 / 1M tokens base unit):
52
54
 
53
- ### Option 2: Install directly from GitHub
54
- ```bash
55
- pi install git:github.com/madgagarin/pi-agentrouter
56
- ```
55
+ | Model | Provider | Context | Output | Reasoning | Input / 1M | Output / 1M | Quota Policy |
56
+ | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
57
+ | `deepseek-v4-flash` | `agentrouter-openai` / `agentrouter-clode` | 1M | 64K | Yes | $4.00 | $12.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
+ | `claude-opus-5` | `agentrouter-clode` | 1M | 64K | Yes (Adaptive) | $6.00 | $30.00 | Daily batch drops |
62
+ | `claude-opus-4-8` | `agentrouter-clode` | 1M | 64K | Yes (Adaptive) | $8.00 | $40.00 | Daily batch drops |
57
63
 
58
- ### Option 3: Local Installation
59
- ```bash
60
- pi install /path/to/pi-agentrouter
61
- ```
64
+ *Note: Claude and GPT models are released in daily batches on AgentRouter. When a batch is exhausted (HTTP 402), use `/agentrouter check` to monitor status or switch to `deepseek-v4-flash` / `glm-5.3` for unrestricted usage.*
62
65
 
63
66
  ---
64
67
 
65
- ## βš™οΈ Configuration
68
+ ## In-Chat Commands
66
69
 
67
- ### 1. Set Your API Key
70
+ | Command | Description |
71
+ | :--- | :--- |
72
+ | `/agentrouter` | Show active model, current monthly spend, extension ordering, and pacing delay. |
73
+ | `/agentrouter check` | Probe model availability (200 OK vs 402) and display monthly usage. |
74
+ | `/agentrouter pricing` | Display current pricing table from [agentrouter.org](https://agentrouter.org). |
75
+ | `/agentrouter sync` | Sync active flagship models into `enabledModels` in `settings.json`. |
76
+ | `/agentrouter key <key>` | Set API key and store it in `agentrouter.json` and `auth.json`. |
77
+ | `/agentrouter pacing <ms>` | Configure delay between requests (default: `3500` ms). |
78
+ | `/agentrouter fix-order` | Reorder extension before `pi-cache-optimizer` in `settings.json` if necessary. |
79
+ | `/compact` | Compact conversation history while preserving required gateway headers. |
68
80
 
69
- **Option A: Inside Pi Chat (Easiest)**
70
- Simply set it directly inside your interactive Pi chat session:
71
- ```text
72
- /agentrouter key sk-your-agentrouter-key
73
- ```
81
+ ---
74
82
 
75
- **Option B: Environment Variable**
76
- Alternatively, export the environment variable in your `~/.bashrc` or `~/.zshrc`:
77
- ```bash
78
- export AGENTROUTER_API_KEY="sk-your-agentrouter-key"
79
- ```
83
+ ## Keybindings
84
+
85
+ | Shortcut | Action |
86
+ | :--- | :--- |
87
+ | `Ctrl + P` | Cycle to next model (`deepseek-v4-flash` βž” `gpt-6-astra` βž” `gpt-5.6-sol` βž” `claude-opus-5` βž” `claude-opus-4-8`) |
88
+ | `Shift + Ctrl + P` | Cycle to previous model |
89
+ | `Shift + Tab` | Toggle reasoning depth (`off` βž” `minimal` βž” `low` βž” `medium` βž” `high`) |
90
+ | `Ctrl + T` | Toggle reasoning block visibility |
91
+ | `Ctrl + L` | Fuzzy-search model picker |
80
92
 
81
93
  ---
82
94
 
83
- ### 2. Configure Model Cycling (`settings.json`)
95
+ ## Recommended `settings.json`
84
96
 
85
- To enable quick model cycling with `Ctrl+P` and set default thinking levels, add the following to `~/.pi/agent/settings.json`:
97
+ Add this to `~/.pi/agent/settings.json` for model switching:
86
98
 
87
99
  ```json
88
100
  {
89
101
  "defaultProvider": "agentrouter-openai",
90
- "defaultModel": "gpt-5.6-sol",
91
- "defaultThinkingLevel": "medium",
102
+ "defaultModel": "deepseek-v4-flash",
103
+ "defaultThinkingLevel": "low",
92
104
  "enabledModels": [
105
+ "agentrouter-openai/deepseek-v4-flash",
106
+ "agentrouter-openai/gpt-6-astra",
93
107
  "agentrouter-openai/gpt-5.6-sol",
94
- "agentrouter-openai/deepseek-v4f",
95
- "agentrouter-clode/claude-opus-4-8",
96
- "agentrouter-clode/claude-opus-5"
108
+ "agentrouter-clode/claude-opus-5",
109
+ "agentrouter-clode/claude-opus-4-8"
97
110
  ]
98
111
  }
99
112
  ```
100
113
 
101
114
  ---
102
115
 
103
- ## ⌨️ Hotkeys & Shortcuts
116
+ ## Notes & FAQ
104
117
 
105
- | Shortcut | Action | Description |
106
- | :--- | :--- | :--- |
107
- | `Ctrl + P` | πŸ”„ **Next Model** | Cycles to the next model (`gpt-5.6-sol` βž” `deepseek-v4f` βž” `claude-opus-4-8` βž” `claude-opus-5`). |
108
- | `Shift + Ctrl + P` | πŸ”„ **Previous Model** | Cycles to the previous model. |
109
- | `Shift + Tab` | 🧠 **Cycle Thinking Level** | Toggles reasoning depth: `off` βž” `minimal` βž” `low` βž” `medium` βž” `high`. |
110
- | `Ctrl + T` | πŸ‘ **Toggle Thinking Visibility** | Collapses or expands thinking/reasoning blocks on screen. |
111
- | `Ctrl + L` | πŸ“‹ **Model Selector** | Opens interactive fuzzy-search model picker. |
118
+ #### How does quota batching work on Claude / GPT?
119
+ AgentRouter releases daily quotas for Claude Opus and GPT-5.6 in batches throughout the day. When a batch is fully consumed, the API returns `402`. Run `/agentrouter check` to see if a batch is active, or use `deepseek-v4-flash` / `glm-5.3` which have unlimited capacity.
112
120
 
113
- ---
121
+ #### Does pacing affect other models?
122
+ No. Request pacing only applies when talking to `agentrouter.org` endpoints. Local models or direct OpenAI/Google providers run at full speed.
114
123
 
115
- ## πŸ› οΈ In-Chat Commands
116
-
117
- * `/agentrouter` β€” View current plugin status, active model, priority position, masked key, and pacing interval.
118
- * `/agentrouter key <your-key>` β€” Update API key for all AgentRouter models on the fly.
119
- * `/agentrouter pacing <ms>` β€” Adjust the minimum delay between consecutive requests (default: `3500` ms).
120
- * `/agentrouter fix-order` β€” Automatically reorder `settings.json` packages to place this plugin directly before `pi-cache-optimizer`.
121
- * `/compact` β€” Compress conversation history safely without 401 authorization errors.
122
-
123
- ---
124
-
125
- ## ❓ FAQ & Troubleshooting
126
-
127
- #### Q: Why does `/compact` fail on raw proxy configurations?
128
- AgentRouter performs client fingerprint verification. Raw summarization requests without Pi's standard prompt signatures get rejected with `401 unauthorized client`. This plugin intercepts the `session_before_compact` event and automatically injects proper authentication signatures.
129
-
130
- #### Q: Does the 3.5s pacing delay affect local or other cloud models?
131
- No. The pacing logic specifically filters for AgentRouter endpoints (`isAgentRouter`). Native OpenAI, Anthropic, Gemini, or local models run at full speed without delay.
132
-
133
- #### Q: How to use custom subagents with AgentRouter?
134
- AgentRouter strictly verifies client authenticity (`pi-code` / `claude-code` prompt signature). If you define custom subagents in extensions like `pi-subagents`, make sure to specify `systemPromptMode: append` in your agent definition frontmatter so the base Pi system prompt identity is preserved.
124
+ #### Using custom subagents (`pi-subagents`)
125
+ AgentRouter requires the base `pi-code` prompt signature for authentication. If you create custom subagents in `~/.pi/agent/agents/*.md`, make sure their frontmatter uses `systemPromptMode: append`.
135
126
 
136
127
  ---
137
128
 
138
- ## πŸ“„ License
129
+ ## License
139
130
 
140
131
  MIT Β© [madgagarin](https://github.com/madgagarin)
132
+
package/index.ts CHANGED
@@ -5,16 +5,185 @@ import * as path from "path";
5
5
 
6
6
  const CONFIG_FILE = path.join(process.env.HOME || "", ".pi/agent/agentrouter.json");
7
7
  const SETTINGS_FILE = path.join(process.env.HOME || "", ".pi/agent/settings.json");
8
+ const PACING_FILE = path.join(process.env.HOME || "", ".pi/agent/.agentrouter-pacing");
9
+ const MODELS_CACHE_FILE = path.join(process.env.HOME || "", ".pi/agent/.agentrouter-models-cache.json");
8
10
 
9
11
  export interface AgentRouterConfig {
10
12
  apiKey?: string;
11
13
  minIntervalMs?: number;
12
14
  }
13
15
 
16
+ export interface ModelSpec {
17
+ id: string;
18
+ name: string;
19
+ providerType: "openai" | "anthropic";
20
+ contextWindow: number;
21
+ maxTokens: number;
22
+ reasoning: boolean;
23
+ compat?: Record<string, any>;
24
+ cost?: {
25
+ input: number;
26
+ output: number;
27
+ cacheRead: number;
28
+ cacheWrite: number;
29
+ };
30
+ }
31
+
32
+ export interface ApiPricingModel {
33
+ model_name: string;
34
+ quota_type: number;
35
+ model_ratio: number;
36
+ model_price: number;
37
+ owner_by?: string;
38
+ completion_ratio: number;
39
+ enable_groups: string[];
40
+ supported_endpoint_types: string[];
41
+ }
42
+
43
+ export const KNOWN_MODEL_SPECS: Record<string, ModelSpec> = {
44
+ "gpt-6-astra": {
45
+ id: "gpt-6-astra",
46
+ name: "gpt-6-astra",
47
+ providerType: "openai",
48
+ contextWindow: 1048576,
49
+ maxTokens: 131072,
50
+ reasoning: true,
51
+ compat: { sendSessionAffinityHeaders: true },
52
+ cost: { input: 3.0 / 1_000_000, output: 15.0 / 1_000_000, cacheRead: 0, cacheWrite: 0 },
53
+ },
54
+ "deepseek-v4-flash": {
55
+ id: "deepseek-v4-flash",
56
+ name: "deepseek-v4-flash",
57
+ providerType: "openai",
58
+ contextWindow: 1048576,
59
+ maxTokens: 65536,
60
+ reasoning: true,
61
+ compat: { sendSessionAffinityHeaders: true },
62
+ cost: { input: 4.0 / 1_000_000, output: 12.0 / 1_000_000, cacheRead: 0, cacheWrite: 0 },
63
+ },
64
+ "deepseek-v4f": {
65
+ id: "deepseek-v4f",
66
+ name: "deepseek-v4f",
67
+ providerType: "openai",
68
+ contextWindow: 1048576,
69
+ maxTokens: 65536,
70
+ reasoning: true,
71
+ compat: { sendSessionAffinityHeaders: true },
72
+ cost: { input: 4.0 / 1_000_000, output: 12.0 / 1_000_000, cacheRead: 0, cacheWrite: 0 },
73
+ },
74
+ "glm-5.3": {
75
+ id: "glm-5.3",
76
+ name: "glm-5.3",
77
+ providerType: "openai",
78
+ contextWindow: 1048576,
79
+ maxTokens: 131072,
80
+ reasoning: true,
81
+ compat: { sendSessionAffinityHeaders: true },
82
+ cost: { input: 3.0 / 1_000_000, output: 12.0 / 1_000_000, cacheRead: 0, cacheWrite: 0 },
83
+ },
84
+ "glm-5.2": {
85
+ id: "glm-5.2",
86
+ name: "glm-5.2",
87
+ providerType: "openai",
88
+ contextWindow: 1048576,
89
+ maxTokens: 131072,
90
+ reasoning: true,
91
+ compat: { sendSessionAffinityHeaders: true },
92
+ cost: { input: 3.0 / 1_000_000, output: 12.0 / 1_000_000, cacheRead: 0, cacheWrite: 0 },
93
+ },
94
+ "gpt-5.6-sol": {
95
+ id: "gpt-5.6-sol",
96
+ name: "gpt-5.6-sol",
97
+ providerType: "openai",
98
+ contextWindow: 1048576,
99
+ maxTokens: 131072,
100
+ reasoning: true,
101
+ compat: { sendSessionAffinityHeaders: true },
102
+ cost: { input: 3.0 / 1_000_000, output: 15.0 / 1_000_000, cacheRead: 0, cacheWrite: 0 },
103
+ },
104
+ "gpt-5.5": {
105
+ id: "gpt-5.5",
106
+ name: "gpt-5.5",
107
+ providerType: "openai",
108
+ contextWindow: 1048576,
109
+ maxTokens: 131072,
110
+ reasoning: true,
111
+ compat: { sendSessionAffinityHeaders: true },
112
+ cost: { input: 4.0 / 1_000_000, output: 8.0 / 1_000_000, cacheRead: 0, cacheWrite: 0 },
113
+ },
114
+ "claude-opus-4-8": {
115
+ id: "claude-opus-4-8",
116
+ name: "claude-opus-4-8",
117
+ providerType: "anthropic",
118
+ contextWindow: 1048576,
119
+ maxTokens: 65536,
120
+ reasoning: true,
121
+ compat: {
122
+ forceAdaptiveThinking: true,
123
+ allowEmptySignature: true,
124
+ sendSessionAffinityHeaders: true,
125
+ supportsEagerToolInputStreaming: false,
126
+ },
127
+ cost: { input: 8.0 / 1_000_000, output: 40.0 / 1_000_000, cacheRead: 0, cacheWrite: 0 },
128
+ },
129
+ "claude-opus-5": {
130
+ id: "claude-opus-5",
131
+ name: "claude-opus-5",
132
+ providerType: "anthropic",
133
+ contextWindow: 1048576,
134
+ maxTokens: 65536,
135
+ reasoning: true,
136
+ compat: {
137
+ forceAdaptiveThinking: true,
138
+ allowEmptySignature: true,
139
+ sendSessionAffinityHeaders: true,
140
+ supportsEagerToolInputStreaming: false,
141
+ },
142
+ cost: { input: 6.0 / 1_000_000, output: 30.0 / 1_000_000, cacheRead: 0, cacheWrite: 0 },
143
+ },
144
+ "claude-opus-4-7": {
145
+ id: "claude-opus-4-7",
146
+ name: "claude-opus-4-7",
147
+ providerType: "anthropic",
148
+ contextWindow: 1048576,
149
+ maxTokens: 65536,
150
+ reasoning: true,
151
+ compat: {
152
+ forceAdaptiveThinking: true,
153
+ allowEmptySignature: true,
154
+ sendSessionAffinityHeaders: true,
155
+ supportsEagerToolInputStreaming: false,
156
+ },
157
+ cost: { input: 8.0 / 1_000_000, output: 40.0 / 1_000_000, cacheRead: 0, cacheWrite: 0 },
158
+ },
159
+ "claude-opus-4-6": {
160
+ id: "claude-opus-4-6",
161
+ name: "claude-opus-4-6",
162
+ providerType: "anthropic",
163
+ contextWindow: 1048576,
164
+ maxTokens: 65536,
165
+ reasoning: true,
166
+ compat: {
167
+ forceAdaptiveThinking: true,
168
+ allowEmptySignature: true,
169
+ sendSessionAffinityHeaders: true,
170
+ supportsEagerToolInputStreaming: false,
171
+ },
172
+ cost: { input: 2.0 / 1_000_000, output: 10.0 / 1_000_000, cacheRead: 0, cacheWrite: 0 },
173
+ },
174
+ };
175
+
14
176
  export function loadConfig(): AgentRouterConfig {
15
177
  try {
16
178
  if (fs.existsSync(CONFIG_FILE)) {
17
- return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
179
+ const cfg = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
180
+ if (cfg.apiKey) return cfg;
181
+ }
182
+ const authPath = path.join(process.env.HOME || "", ".pi/agent/auth.json");
183
+ if (fs.existsSync(authPath)) {
184
+ const auth = JSON.parse(fs.readFileSync(authPath, "utf-8"));
185
+ const key = auth["agentrouter-openai"]?.key || auth["agentrouter-clode"]?.key;
186
+ if (key) return { apiKey: key };
18
187
  }
19
188
  } catch {}
20
189
  return {};
@@ -27,6 +196,19 @@ export function saveConfig(cfg: AgentRouterConfig): void {
27
196
  fs.mkdirSync(dir, { recursive: true });
28
197
  }
29
198
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2), "utf-8");
199
+
200
+ if (cfg.apiKey) {
201
+ const authPath = path.join(process.env.HOME || "", ".pi/agent/auth.json");
202
+ let auth: Record<string, any> = {};
203
+ if (fs.existsSync(authPath)) {
204
+ try {
205
+ auth = JSON.parse(fs.readFileSync(authPath, "utf-8"));
206
+ } catch {}
207
+ }
208
+ auth["agentrouter-openai"] = { type: "api_key", key: cfg.apiKey };
209
+ auth["agentrouter-clode"] = { type: "api_key", key: cfg.apiKey };
210
+ fs.writeFileSync(authPath, JSON.stringify(auth, null, 2), "utf-8");
211
+ }
30
212
  } catch {}
31
213
  }
32
214
 
@@ -35,13 +217,29 @@ export function normalizeApiKey(key?: string): string {
35
217
  return key.trim().replace(/^["']|["']$/g, "").trim();
36
218
  }
37
219
 
38
- export interface PackageOrderState {
39
- agentRouterIndex: number;
40
- cacheOptimizerIndex: number;
41
- needsFix: boolean;
220
+ export function loadCachedPricing(): ApiPricingModel[] | null {
221
+ try {
222
+ if (fs.existsSync(MODELS_CACHE_FILE)) {
223
+ const data = JSON.parse(fs.readFileSync(MODELS_CACHE_FILE, "utf-8"));
224
+ if (Array.isArray(data) && data.length > 0) {
225
+ return data;
226
+ }
227
+ }
228
+ } catch {}
229
+ return null;
230
+ }
231
+
232
+ export function saveCachedPricing(models: ApiPricingModel[]): void {
233
+ try {
234
+ const dir = path.dirname(MODELS_CACHE_FILE);
235
+ if (!fs.existsSync(dir)) {
236
+ fs.mkdirSync(dir, { recursive: true });
237
+ }
238
+ fs.writeFileSync(MODELS_CACHE_FILE, JSON.stringify(models, null, 2), "utf-8");
239
+ } catch {}
42
240
  }
43
241
 
44
- export function getPackageOrderState(): PackageOrderState {
242
+ export function getPackageOrderState(): { agentRouterIndex: number; cacheOptimizerIndex: number; needsFix: boolean } {
45
243
  try {
46
244
  if (!fs.existsSync(SETTINGS_FILE)) {
47
245
  return { agentRouterIndex: -1, cacheOptimizerIndex: -1, needsFix: false };
@@ -50,12 +248,10 @@ export function getPackageOrderState(): PackageOrderState {
50
248
  if (!Array.isArray(settings.packages)) {
51
249
  return { agentRouterIndex: -1, cacheOptimizerIndex: -1, needsFix: false };
52
250
  }
53
- const arIdx = settings.packages.findIndex((p: string) =>
54
- typeof p === "string" && (p.includes("@madgagarin/pi-agentrouter") || p.includes("pi-agentrouter"))
55
- );
56
- const cacheIdx = settings.packages.findIndex((p: string) =>
57
- typeof p === "string" && p.includes("pi-cache-optimizer")
251
+ const arIdx = settings.packages.findIndex(
252
+ (p: string) => typeof p === "string" && (p.includes("@madgagarin/pi-agentrouter") || p.includes("pi-agentrouter"))
58
253
  );
254
+ const cacheIdx = settings.packages.findIndex((p: string) => typeof p === "string" && p.includes("pi-cache-optimizer"));
59
255
  const needsFix = cacheIdx !== -1 && arIdx !== -1 && arIdx > cacheIdx;
60
256
  return { agentRouterIndex: arIdx, cacheOptimizerIndex: cacheIdx, needsFix };
61
257
  } catch {
@@ -68,16 +264,14 @@ export function fixPackagePriorityInSettings(): boolean {
68
264
  if (!fs.existsSync(SETTINGS_FILE)) return false;
69
265
  const settings = JSON.parse(fs.readFileSync(SETTINGS_FILE, "utf-8"));
70
266
  if (!Array.isArray(settings.packages)) return false;
71
- const arIdx = settings.packages.findIndex((p: string) =>
72
- typeof p === "string" && (p.includes("@madgagarin/pi-agentrouter") || p.includes("pi-agentrouter"))
73
- );
74
- const cacheIdx = settings.packages.findIndex((p: string) =>
75
- typeof p === "string" && p.includes("pi-cache-optimizer")
267
+ const arIdx = settings.packages.findIndex(
268
+ (p: string) => typeof p === "string" && (p.includes("@madgagarin/pi-agentrouter") || p.includes("pi-agentrouter"))
76
269
  );
270
+ const cacheIdx = settings.packages.findIndex((p: string) => typeof p === "string" && p.includes("pi-cache-optimizer"));
77
271
  if (cacheIdx !== -1 && arIdx !== -1 && arIdx > cacheIdx) {
78
272
  const pkg = settings.packages.splice(arIdx, 1)[0];
79
- const targetCacheIdx = settings.packages.findIndex((p: string) =>
80
- typeof p === "string" && p.includes("pi-cache-optimizer")
273
+ const targetCacheIdx = settings.packages.findIndex(
274
+ (p: string) => typeof p === "string" && p.includes("pi-cache-optimizer")
81
275
  );
82
276
  settings.packages.splice(targetCacheIdx, 0, pkg);
83
277
  fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), "utf-8");
@@ -87,10 +281,35 @@ export function fixPackagePriorityInSettings(): boolean {
87
281
  return false;
88
282
  }
89
283
 
90
- const initialConfig = loadConfig();
91
- let currentApiKey = normalizeApiKey(process.env.AGENTROUTER_API_KEY || initialConfig.apiKey || "");
92
- let minIntervalMs = initialConfig.minIntervalMs ?? 3500;
93
- const PACING_FILE = path.join(process.env.HOME || "", ".pi/agent/.agentrouter-pacing");
284
+ export const FLAGSHIP_MODELS: string[] = [
285
+ "agentrouter-openai/deepseek-v4-flash",
286
+ "agentrouter-openai/gpt-6-astra",
287
+ "agentrouter-openai/gpt-5.6-sol",
288
+ "agentrouter-clode/claude-opus-5",
289
+ "agentrouter-clode/claude-opus-4-8",
290
+ ];
291
+
292
+ export function syncEnabledModelsInSettings(): { added: string[]; count: number } {
293
+ try {
294
+ if (!fs.existsSync(SETTINGS_FILE)) return { added: [], count: 0 };
295
+ const settings = JSON.parse(fs.readFileSync(SETTINGS_FILE, "utf-8"));
296
+ if (!Array.isArray(settings.enabledModels)) return { added: [], count: 0 };
297
+
298
+ const added: string[] = [];
299
+ for (const m of FLAGSHIP_MODELS) {
300
+ if (!settings.enabledModels.includes(m)) {
301
+ settings.enabledModels.push(m);
302
+ added.push(m);
303
+ }
304
+ }
305
+ if (added.length > 0) {
306
+ fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), "utf-8");
307
+ }
308
+ return { added, count: settings.enabledModels.length };
309
+ } catch {
310
+ return { added: [], count: 0 };
311
+ }
312
+ }
94
313
 
95
314
  export function getLastRequestEndTime(): number {
96
315
  try {
@@ -121,13 +340,6 @@ export function isAgentRouter(providerName?: string, baseUrl?: string): boolean
121
340
  export const CANONICAL_PI_HEADER =
122
341
  "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.";
123
342
 
124
- /**
125
- * Enforces that the canonical pi-code system prompt signature is strictly at index 0.
126
- * If another plugin or wrapper prepended text before the canonical header, it reorders
127
- * the header to the very top and shifts the injected prefix right after it.
128
- * If the header is missing entirely (e.g. from replace mode), it prepends the canonical header.
129
- * This guarantees both WAF client authentication and stable prompt cache prefix matching.
130
- */
131
343
  export function enforceCanonicalRootPrompt(systemPrompt: string | any[] | undefined): string | any[] {
132
344
  if (!systemPrompt) {
133
345
  return CANONICAL_PI_HEADER;
@@ -165,8 +377,337 @@ export function enforceCanonicalRootPrompt(systemPrompt: string | any[] | undefi
165
377
  return systemPrompt;
166
378
  }
167
379
 
380
+ export function cleanJsonSchemaObject(schema: any): void {
381
+ if (!schema || typeof schema !== "object") return;
382
+
383
+ if (schema.type === "object" || schema.properties) {
384
+ if (schema.required === null || schema.required === undefined || !Array.isArray(schema.required)) {
385
+ schema.required = [];
386
+ }
387
+ }
388
+
389
+ if (schema.properties && typeof schema.properties === "object") {
390
+ for (const [propName, propDef] of Object.entries(schema.properties)) {
391
+ if (propDef && typeof propDef === "object") {
392
+ cleanJsonSchemaObject(propDef);
393
+ }
394
+ }
395
+ }
396
+
397
+ if (schema.items) {
398
+ if (typeof schema.items === "object") {
399
+ cleanJsonSchemaObject(schema.items);
400
+ } else if (schema.items === null) {
401
+ delete schema.items;
402
+ }
403
+ }
404
+ }
405
+
406
+ export function sanitizeOpenAiTools(tools: any[]): void {
407
+ if (!Array.isArray(tools)) return;
408
+ for (const tool of tools) {
409
+ if (!tool || typeof tool !== "object") continue;
410
+ const fn = tool.function || tool;
411
+ if (fn.parameters && typeof fn.parameters === "object") {
412
+ cleanJsonSchemaObject(fn.parameters);
413
+ }
414
+ }
415
+ }
416
+
417
+ export function normalizeMessagesForAgentRouter(messages: any[]): void {
418
+ if (!Array.isArray(messages)) return;
419
+ for (const msg of messages) {
420
+ if (!msg || typeof msg !== "object") continue;
421
+ if (msg.role === "developer") {
422
+ msg.role = "system";
423
+ }
424
+ if (msg.role === "assistant") {
425
+ let extractedThinking: string | undefined;
426
+
427
+ if (Array.isArray(msg.content)) {
428
+ const thinkingParts: string[] = [];
429
+ const nonThinkingParts: any[] = [];
430
+
431
+ for (const part of msg.content) {
432
+ if (part && typeof part === "object" && (part.type === "thinking" || part.type === "reasoning")) {
433
+ const text = part.thinking || part.text;
434
+ if (text) thinkingParts.push(text);
435
+ } else {
436
+ nonThinkingParts.push(part);
437
+ }
438
+ }
439
+
440
+ if (thinkingParts.length > 0) {
441
+ extractedThinking = thinkingParts.join("\n");
442
+ }
443
+
444
+ if (nonThinkingParts.length === 0) {
445
+ msg.content = "";
446
+ } else if (nonThinkingParts.length === 1 && nonThinkingParts[0].type === "text") {
447
+ msg.content = nonThinkingParts[0].text;
448
+ } else {
449
+ msg.content = nonThinkingParts;
450
+ }
451
+ }
452
+
453
+ if (extractedThinking && !msg.reasoning_content) {
454
+ msg.reasoning_content = extractedThinking;
455
+ }
456
+
457
+ // If assistant executed tool calls, AgentRouter DeepSeek proxy strictly requires reasoning_content
458
+ if (Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0 && !msg.reasoning_content) {
459
+ msg.reasoning_content = "Executing tools...";
460
+ }
461
+ }
462
+ }
463
+ }
464
+
465
+ let lastWarnedErrorTimestamp = 0;
466
+ export function checkAndNotifyContentBlocked(errMessage: string | undefined, ctx: any): void {
467
+ if (!errMessage) return;
468
+ const now = Date.now();
469
+ if (now - lastWarnedErrorTimestamp < 2000) return;
470
+ if (errMessage.includes("content-blocked")) {
471
+ lastWarnedErrorTimestamp = now;
472
+ const tip = "[AgentRouter] Request was blocked by upstream gateway content filter (content-blocked).";
473
+ if (ctx?.hasUI) {
474
+ ctx.ui.notify(tip, "warning");
475
+ } else {
476
+ console.warn(`\n⚠️ ${tip}\n`);
477
+ }
478
+ }
479
+ }
480
+
481
+ export async function fetchLivePricing(): Promise<ApiPricingModel[] | null> {
482
+ try {
483
+ const res = await fetch("https://agentrouter.org/api/pricing", {
484
+ headers: { "User-Agent": "pi-code" },
485
+ });
486
+ if (!res.ok) return null;
487
+ const data = await res.json();
488
+ if (data && Array.isArray(data.data) && data.data.length > 0) {
489
+ return data.data;
490
+ }
491
+ } catch {}
492
+ return null;
493
+ }
494
+
495
+ export async function fetchTokenUsage(apiKey: string): Promise<number | null> {
496
+ if (!apiKey) return null;
497
+ try {
498
+ const now = new Date();
499
+ const year = now.getFullYear();
500
+ const month = String(now.getMonth() + 1).padStart(2, "0");
501
+ const startDate = `${year}-${month}-01`;
502
+ const endDate = `${year}-${month}-31`;
503
+ const res = await fetch(
504
+ `https://agentrouter.org/v1/dashboard/billing/usage?start_date=${startDate}&end_date=${endDate}`,
505
+ {
506
+ headers: {
507
+ Authorization: `Bearer ${apiKey}`,
508
+ "User-Agent": "pi-code",
509
+ },
510
+ }
511
+ );
512
+ if (!res.ok) return null;
513
+ const data = await res.json();
514
+ if (data && typeof data.total_usage === "number") {
515
+ return data.total_usage;
516
+ }
517
+ } catch {}
518
+ return null;
519
+ }
520
+
521
+ export interface ModelProbeResult {
522
+ model: string;
523
+ status: "READY" | "QUOTA_EXHAUSTED" | "FORBIDDEN" | "ERROR";
524
+ code: number;
525
+ message?: string;
526
+ }
527
+
528
+ export async function probeModelQuota(modelId: string, apiKey: string, isAnthropic: boolean): Promise<ModelProbeResult> {
529
+ const url = isAnthropic ? "https://agentrouter.org/v1/messages" : "https://agentrouter.org/v1/chat/completions";
530
+ const headers = isAnthropic
531
+ ? {
532
+ "Content-Type": "application/json",
533
+ "x-api-key": apiKey,
534
+ "anthropic-version": "2023-06-01",
535
+ "User-Agent": "pi-code",
536
+ }
537
+ : {
538
+ "Content-Type": "application/json",
539
+ Authorization: `Bearer ${apiKey}`,
540
+ "User-Agent": "pi-code",
541
+ };
542
+
543
+ const body = isAnthropic
544
+ ? {
545
+ model: modelId,
546
+ system: CANONICAL_PI_HEADER,
547
+ messages: [{ role: "user", content: "ping" }],
548
+ max_tokens: 1,
549
+ }
550
+ : {
551
+ model: modelId,
552
+ messages: [
553
+ { role: "system", content: CANONICAL_PI_HEADER },
554
+ { role: "user", content: "ping" },
555
+ ],
556
+ max_tokens: 1,
557
+ };
558
+
559
+ try {
560
+ const res = await fetch(url, {
561
+ method: "POST",
562
+ headers,
563
+ body: JSON.stringify(body),
564
+ });
565
+
566
+ if (res.ok) {
567
+ return { model: modelId, status: "READY", code: res.status };
568
+ }
569
+
570
+ if (
571
+ res.status === 402 ||
572
+ msg.toLowerCase().includes("quota") ||
573
+ msg.toLowerCase().includes("exhausted") ||
574
+ msg.toLowerCase().includes("budget pool")
575
+ ) {
576
+ return { model: modelId, status: "QUOTA_EXHAUSTED", code: 402, message: msg };
577
+ }
578
+ if (res.status === 403) {
579
+ return { model: modelId, status: "FORBIDDEN", code: 403, message: msg };
580
+ }
581
+ return { model: modelId, status: "ERROR", code: res.status, message: msg };
582
+ } catch (err: any) {
583
+ return { model: modelId, status: "ERROR", code: 0, message: err.message };
584
+ }
585
+ }
586
+
168
587
  export default function (pi: ExtensionAPI) {
169
- function registerAgentRouterProviders(apiKey: string): void {
588
+ function getEffectiveApiKey(): string {
589
+ const cfg = loadConfig();
590
+ return normalizeApiKey(process.env.AGENTROUTER_API_KEY || process.env.AGENT_ROUTER_API_KEY || cfg.apiKey || "");
591
+ }
592
+
593
+ const initialConfig = loadConfig();
594
+ let currentApiKey = getEffectiveApiKey();
595
+ let minIntervalMs = initialConfig.minIntervalMs ?? 3500;
596
+
597
+ function buildModelsFromPricing(apiPricing: ApiPricingModel[] | null): {
598
+ openaiModels: any[];
599
+ claudeModels: any[];
600
+ newModels: string[];
601
+ } {
602
+ const openaiModels: any[] = [];
603
+ const claudeModels: any[] = [];
604
+ const newModels: string[] = [];
605
+ const processed = new Set<string>();
606
+
607
+ if (apiPricing && apiPricing.length > 0) {
608
+ for (const item of apiPricing) {
609
+ const id = item.model_name;
610
+ processed.add(id);
611
+ const spec = KNOWN_MODEL_SPECS[id];
612
+
613
+ const inCost = (item.model_ratio * 2.0) / 1_000_000;
614
+ const outCost = (item.model_ratio * item.completion_ratio * 2.0) / 1_000_000;
615
+
616
+ if (spec) {
617
+ const modelObj = {
618
+ id: spec.id,
619
+ name: spec.name,
620
+ reasoning: spec.reasoning,
621
+ input: ["text"],
622
+ contextWindow: spec.contextWindow,
623
+ maxTokens: spec.maxTokens,
624
+ cost: { input: inCost, output: outCost, cacheRead: 0, cacheWrite: 0 },
625
+ compat: spec.compat || { sendSessionAffinityHeaders: true },
626
+ };
627
+ if (spec.providerType === "anthropic") {
628
+ claudeModels.push(modelObj);
629
+ } else {
630
+ openaiModels.push(modelObj);
631
+ }
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
+ } else {
644
+ newModels.push(id);
645
+ const isAnthropic =
646
+ item.supported_endpoint_types.includes("anthropic") && !item.supported_endpoint_types.includes("openai");
647
+ const modelObj = {
648
+ id,
649
+ name: id,
650
+ reasoning: true,
651
+ input: ["text"],
652
+ contextWindow: 131072,
653
+ maxTokens: 16384,
654
+ cost: { input: inCost, output: outCost, cacheRead: 0, cacheWrite: 0 },
655
+ compat: isAnthropic
656
+ ? {
657
+ forceAdaptiveThinking: true,
658
+ allowEmptySignature: true,
659
+ sendSessionAffinityHeaders: true,
660
+ }
661
+ : {
662
+ sendSessionAffinityHeaders: true,
663
+ },
664
+ };
665
+ if (isAnthropic) {
666
+ claudeModels.push(modelObj);
667
+ } else {
668
+ openaiModels.push(modelObj);
669
+ }
670
+ }
671
+ }
672
+ }
673
+
674
+ for (const [id, spec] of Object.entries(KNOWN_MODEL_SPECS)) {
675
+ if (!processed.has(id)) {
676
+ const modelObj = {
677
+ id: spec.id,
678
+ name: spec.name,
679
+ reasoning: spec.reasoning,
680
+ input: ["text"],
681
+ contextWindow: spec.contextWindow,
682
+ maxTokens: spec.maxTokens,
683
+ cost: spec.cost || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
684
+ compat: spec.compat || { sendSessionAffinityHeaders: true },
685
+ };
686
+ if (spec.providerType === "anthropic") {
687
+ claudeModels.push(modelObj);
688
+ } else {
689
+ openaiModels.push(modelObj);
690
+ }
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
+ }
703
+ }
704
+
705
+ return { openaiModels, claudeModels, newModels };
706
+ }
707
+
708
+ function registerAgentRouterProviders(apiKey: string, pricingData: ApiPricingModel[] | null): string[] {
709
+ const { openaiModels, claudeModels, newModels } = buildModelsFromPricing(pricingData);
710
+
170
711
  pi.registerProvider("agentrouter-openai", {
171
712
  name: "AgentRouter OpenAI",
172
713
  baseUrl: "https://agentrouter.org/v1",
@@ -175,32 +716,7 @@ export default function (pi: ExtensionAPI) {
175
716
  compat: {
176
717
  sendSessionAffinityHeaders: true,
177
718
  },
178
- models: [
179
- {
180
- id: "gpt-5.6-sol",
181
- name: "gpt-5.6-sol",
182
- reasoning: true,
183
- input: ["text"],
184
- contextWindow: 1048576,
185
- maxTokens: 131072,
186
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
187
- compat: {
188
- sendSessionAffinityHeaders: true,
189
- },
190
- },
191
- {
192
- id: "deepseek-v4f",
193
- name: "deepseek-v4f",
194
- reasoning: true,
195
- input: ["text"],
196
- contextWindow: 131072,
197
- maxTokens: 65536,
198
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
199
- compat: {
200
- sendSessionAffinityHeaders: true,
201
- },
202
- },
203
- ],
719
+ models: openaiModels,
204
720
  });
205
721
 
206
722
  pi.registerProvider("agentrouter-clode", {
@@ -214,42 +730,14 @@ export default function (pi: ExtensionAPI) {
214
730
  sendSessionAffinityHeaders: true,
215
731
  supportsEagerToolInputStreaming: false,
216
732
  },
217
- models: [
218
- {
219
- id: "claude-opus-4-8",
220
- name: "claude-opus-4-8",
221
- reasoning: true,
222
- input: ["text"],
223
- contextWindow: 524288,
224
- maxTokens: 65536,
225
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
226
- compat: {
227
- forceAdaptiveThinking: true,
228
- allowEmptySignature: true,
229
- sendSessionAffinityHeaders: true,
230
- supportsEagerToolInputStreaming: false,
231
- },
232
- },
233
- {
234
- id: "claude-opus-5",
235
- name: "claude-opus-5",
236
- reasoning: true,
237
- input: ["text"],
238
- contextWindow: 1048576,
239
- maxTokens: 65536,
240
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
241
- compat: {
242
- forceAdaptiveThinking: true,
243
- allowEmptySignature: true,
244
- sendSessionAffinityHeaders: true,
245
- supportsEagerToolInputStreaming: false,
246
- },
247
- },
248
- ],
733
+ models: claudeModels,
249
734
  });
735
+
736
+ return newModels;
250
737
  }
251
738
 
252
- registerAgentRouterProviders(currentApiKey);
739
+ const cachedPricing = loadCachedPricing();
740
+ registerAgentRouterProviders(currentApiKey, cachedPricing);
253
741
 
254
742
  function updatePromptRewriteEnvForModel(model?: any): void {
255
743
  if (isAgentRouter(model?.provider, model?.baseUrl)) {
@@ -259,13 +747,11 @@ export default function (pi: ExtensionAPI) {
259
747
  }
260
748
  }
261
749
 
262
- // Inter-request pacing to respect AgentRouter WAF rate limits and enforce canonical root prompt
263
750
  pi.on("before_provider_request", async (event, ctx) => {
264
751
  const provider = ((event as any)?.model?.provider || ctx?.model?.provider || "").toLowerCase();
265
- const baseUrl = ((event as any)?.model?.baseUrl || (ctx?.model as any)?.baseUrl || "");
752
+ const baseUrl = (event as any)?.model?.baseUrl || (ctx?.model as any)?.baseUrl || "";
266
753
 
267
754
  if (isAgentRouter(provider, baseUrl)) {
268
- // 1. Cross-process request pacing
269
755
  const lastEnd = getLastRequestEndTime();
270
756
  const now = Date.now();
271
757
  const elapsed = now - lastEnd;
@@ -274,15 +760,17 @@ export default function (pi: ExtensionAPI) {
274
760
  await new Promise((resolve) => setTimeout(resolve, waitMs));
275
761
  }
276
762
 
277
- // 2. Enforce canonical root system prompt in payload (Anthropic & OpenAI formats)
278
763
  const payload = event.payload;
279
764
  if (payload) {
280
765
  if (payload.system !== undefined) {
281
766
  payload.system = enforceCanonicalRootPrompt(payload.system);
282
767
  }
283
768
  if (Array.isArray(payload.messages) && payload.messages.length > 0) {
769
+ normalizeMessagesForAgentRouter(payload.messages);
770
+
284
771
  const firstMsg = payload.messages[0];
285
- if (firstMsg && firstMsg.role === "system") {
772
+ if (firstMsg && (firstMsg.role === "system" || firstMsg.role === "developer")) {
773
+ firstMsg.role = "system";
286
774
  if (typeof firstMsg.content === "string") {
287
775
  firstMsg.content = enforceCanonicalRootPrompt(firstMsg.content);
288
776
  } else if (Array.isArray(firstMsg.content)) {
@@ -290,22 +778,33 @@ export default function (pi: ExtensionAPI) {
290
778
  }
291
779
  }
292
780
  }
781
+ if (Array.isArray(payload.tools) && payload.tools.length > 0) {
782
+ sanitizeOpenAiTools(payload.tools);
783
+ }
293
784
  }
294
785
  }
295
786
  return undefined;
296
787
  });
297
788
 
298
- pi.on("message_end", async (_event, ctx) => {
789
+ pi.on("message_end", async (event, ctx) => {
299
790
  const model = ctx?.model;
300
791
  if (isAgentRouter(model?.provider, (model as any)?.baseUrl)) {
301
792
  setLastRequestEndTime(Date.now());
793
+ const msg = (event as any)?.message;
794
+ if (msg && msg.role === "assistant" && msg.stopReason === "error") {
795
+ checkAndNotifyContentBlocked(msg.errorMessage, ctx);
796
+ }
302
797
  }
303
798
  });
304
799
 
305
- pi.on("turn_end", async (_event, ctx) => {
800
+ pi.on("turn_end", async (event, ctx) => {
306
801
  const model = ctx?.model;
307
802
  if (isAgentRouter(model?.provider, (model as any)?.baseUrl)) {
308
803
  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
+ }
309
808
  }
310
809
  });
311
810
 
@@ -317,8 +816,25 @@ export default function (pi: ExtensionAPI) {
317
816
  });
318
817
 
319
818
  pi.on("session_start", async (_event, ctx) => {
819
+ currentApiKey = getEffectiveApiKey();
320
820
  updatePromptRewriteEnvForModel(ctx.model);
321
821
  setLastRequestEndTime(Date.now());
822
+ syncEnabledModelsInSettings();
823
+
824
+ fetchLivePricing().then((livePricing) => {
825
+ if (livePricing) {
826
+ saveCachedPricing(livePricing);
827
+ const newModels = registerAgentRouterProviders(currentApiKey, livePricing);
828
+ syncEnabledModelsInSettings();
829
+ if (newModels.length > 0 && ctx.hasUI) {
830
+ ctx.ui.notify(
831
+ `[AgentRouter] Discovered new models on gateway: ${newModels.join(", ")}.\n` +
832
+ `Auto-registered with safe default limits (128K context). Check settings.json or await plugin update for optimized specs.`,
833
+ "info"
834
+ );
835
+ }
836
+ }
837
+ });
322
838
 
323
839
  const order = getPackageOrderState();
324
840
  if (order.needsFix && ctx.hasUI && typeof (ctx.ui as any).confirm === "function") {
@@ -326,13 +842,16 @@ export default function (pi: ExtensionAPI) {
326
842
  const confirmed = await (ctx.ui as any).confirm(
327
843
  "Pi AgentRouter Package Priority",
328
844
  "@madgagarin/pi-agentrouter is listed AFTER pi-cache-optimizer in settings.json packages.\n\n" +
329
- "It must be placed before pi-cache-optimizer so prompt cache bypass takes effect before cache-optimizer transforms the prompt.\n\n" +
330
- "Move @madgagarin/pi-agentrouter directly above pi-cache-optimizer in settings.json?"
845
+ "It must be placed before pi-cache-optimizer so prompt cache bypass takes effect before cache-optimizer transforms the prompt.\n\n" +
846
+ "Move @madgagarin/pi-agentrouter directly above pi-cache-optimizer in settings.json?"
331
847
  );
332
848
  if (confirmed) {
333
849
  const success = fixPackagePriorityInSettings();
334
850
  if (success) {
335
- ctx.ui.notify("@madgagarin/pi-agentrouter moved above pi-cache-optimizer in settings.json. Please restart Pi for changes to take full effect.", "info");
851
+ ctx.ui.notify(
852
+ "@madgagarin/pi-agentrouter moved above pi-cache-optimizer in settings.json. Please restart Pi for changes to take full effect.",
853
+ "info"
854
+ );
336
855
  }
337
856
  }
338
857
  } catch {}
@@ -438,7 +957,7 @@ export default function (pi: ExtensionAPI) {
438
957
  });
439
958
 
440
959
  pi.registerCommand("agentrouter", {
441
- description: "Manage AgentRouter plugin settings (status, API key, pacing interval)",
960
+ description: "Manage AgentRouter plugin settings, live quotas, and pricing (/agentrouter check, /agentrouter pricing)",
442
961
  handler: async (args, ctx) => {
443
962
  const parts = (args || "").trim().split(/\s+/);
444
963
  const action = parts[0]?.toLowerCase();
@@ -452,11 +971,22 @@ export default function (pi: ExtensionAPI) {
452
971
  }
453
972
  currentApiKey = cleanKey;
454
973
  saveConfig({ apiKey: cleanKey, minIntervalMs });
455
- registerAgentRouterProviders(cleanKey);
974
+ registerAgentRouterProviders(cleanKey, loadCachedPricing());
975
+ syncEnabledModelsInSettings();
456
976
  ctx.ui.notify("AgentRouter API key updated successfully for all models.", "info");
457
977
  return;
458
978
  }
459
979
 
980
+ if (action === "sync" || action === "enable-models") {
981
+ const res = syncEnabledModelsInSettings();
982
+ if (res.added.length > 0) {
983
+ ctx.ui.notify(`[AgentRouter] Added to enabledModels: ${res.added.join(", ")} (total: ${res.count}).`, "info");
984
+ } else {
985
+ ctx.ui.notify(`[AgentRouter] All flagship models already enabled in settings.json (total: ${res.count}).`, "info");
986
+ }
987
+ return;
988
+ }
989
+
460
990
  if (action === "fix-order" || action === "order") {
461
991
  const order = getPackageOrderState();
462
992
  if (!order.needsFix) {
@@ -471,7 +1001,10 @@ export default function (pi: ExtensionAPI) {
471
1001
  }
472
1002
  const success = fixPackagePriorityInSettings();
473
1003
  if (success) {
474
- ctx.ui.notify("@madgagarin/pi-agentrouter moved directly above pi-cache-optimizer in settings.json. Please restart Pi for changes to take full effect.", "info");
1004
+ ctx.ui.notify(
1005
+ "@madgagarin/pi-agentrouter moved directly above pi-cache-optimizer in settings.json. Please restart Pi for changes to take full effect.",
1006
+ "info"
1007
+ );
475
1008
  } else {
476
1009
  ctx.ui.notify("Failed to update settings.json.", "error");
477
1010
  }
@@ -490,7 +1023,101 @@ export default function (pi: ExtensionAPI) {
490
1023
  return;
491
1024
  }
492
1025
 
493
- const maskedKey = currentApiKey.length > 8 ? `${currentApiKey.slice(0, 7)}...${currentApiKey.slice(-4)}` : "not set";
1026
+ if (action === "pricing" || action === "price" || action === "models") {
1027
+ ctx.ui.notify("Fetching live pricing from AgentRouter API...", "info");
1028
+ const live = await fetchLivePricing();
1029
+ if (live) {
1030
+ saveCachedPricing(live);
1031
+ registerAgentRouterProviders(currentApiKey, live);
1032
+ }
1033
+ const pricing = live || loadCachedPricing() || [];
1034
+ if (pricing.length === 0) {
1035
+ ctx.ui.notify("Unable to fetch pricing from AgentRouter API. Check internet connection.", "error");
1036
+ return;
1037
+ }
1038
+
1039
+ let tableText =
1040
+ `[AgentRouter Official Pricing]\n` +
1041
+ `Live rates calculated from AgentRouter gateway API (1.0x ratio = $2.00/1M tokens):\n\n` +
1042
+ `Model Input / 1M Output / 1M Ratio (In/Out) Protocol\n` +
1043
+ `------------------------------------------------------------------------\n`;
1044
+
1045
+ for (const item of pricing) {
1046
+ const inPrice = `$${(item.model_ratio * 2.0).toFixed(2)}`;
1047
+ const outPrice = `$${(item.model_ratio * item.completion_ratio * 2.0).toFixed(2)}`;
1048
+ const ratio = `${item.model_ratio.toFixed(1)}x / ${item.completion_ratio.toFixed(1)}x`;
1049
+ const proto = item.supported_endpoint_types.join(", ");
1050
+ tableText += `${item.model_name.padEnd(21)} ${inPrice.padEnd(12)} ${outPrice.padEnd(13)} ${ratio.padEnd(16)} ${proto}\n`;
1051
+ }
1052
+
1053
+ tableText +=
1054
+ `------------------------------------------------------------------------\n` +
1055
+ `* Output cost = Input ratio Γ— completion ratio Γ— $2.00. Zero hidden fees.`;
1056
+
1057
+ ctx.ui.notify(tableText, "info");
1058
+ return;
1059
+ }
1060
+
1061
+ if (action === "check" || action === "quota" || action === "status-live") {
1062
+ currentApiKey = getEffectiveApiKey();
1063
+ if (!currentApiKey) {
1064
+ ctx.ui.notify("No API key configured. Set one with /agentrouter key <your-key>", "error");
1065
+ return;
1066
+ }
1067
+
1068
+ ctx.ui.notify("Probing live model quotas and balance from AgentRouter...", "info");
1069
+ const [usageUsd, pricingList] = await Promise.all([fetchTokenUsage(currentApiKey), fetchLivePricing()]);
1070
+ if (pricingList) {
1071
+ saveCachedPricing(pricingList);
1072
+ }
1073
+
1074
+ const modelsToProbe = pricingList
1075
+ ? pricingList.map((p) => ({
1076
+ id: p.model_name,
1077
+ isAnthropic: p.supported_endpoint_types.includes("anthropic") && !p.supported_endpoint_types.includes("openai"),
1078
+ }))
1079
+ : [
1080
+ { id: "deepseek-v4-flash", isAnthropic: false },
1081
+ { id: "gpt-6-astra", isAnthropic: false },
1082
+ { id: "gpt-5.6-sol", isAnthropic: false },
1083
+ { id: "claude-opus-5", isAnthropic: true },
1084
+ { id: "claude-opus-4-8", isAnthropic: true },
1085
+ ];
1086
+
1087
+ const probeResults = await Promise.all(
1088
+ modelsToProbe.map((m) => probeModelQuota(m.id, currentApiKey, m.isAnthropic))
1089
+ );
1090
+
1091
+ let report = `[AgentRouter Live Quota & Health]\n`;
1092
+ if (usageUsd !== null) {
1093
+ report += `- Total Spent (Current Month): $${usageUsd.toFixed(4)} USD\n\n`;
1094
+ } else {
1095
+ report += `- Token status: Active\n\n`;
1096
+ }
1097
+
1098
+ report += `Model Status:\n`;
1099
+ for (const res of probeResults) {
1100
+ if (res.status === "READY") {
1101
+ report += ` 🟒 ${res.model.padEnd(20)}: Ready (200 OK - Quota available)\n`;
1102
+ } else if (res.status === "QUOTA_EXHAUSTED") {
1103
+ report += ` ⏳ ${res.model.padEnd(20)}: Batch Quota Exhausted (402) - Next batch drop soon\n`;
1104
+ } else if (res.status === "FORBIDDEN") {
1105
+ report += ` πŸ”΄ ${res.model.padEnd(20)}: Forbidden (403) - Token has no permissions\n`;
1106
+ } else {
1107
+ report += ` ⚠️ ${res.model.padEnd(20)}: Error (${res.code}) ${res.message ? "- " + res.message : ""}\n`;
1108
+ }
1109
+ }
1110
+
1111
+ report +=
1112
+ `\nTip: Claude and GPT models use daily batch quotas. If exhausted, switch to DeepSeek V4 Flash which has unlimited availability.`;
1113
+
1114
+ ctx.ui.notify(report, "info");
1115
+ return;
1116
+ }
1117
+
1118
+ currentApiKey = getEffectiveApiKey();
1119
+ const maskedKey =
1120
+ currentApiKey.length > 8 ? `${currentApiKey.slice(0, 7)}...${currentApiKey.slice(-4)}` : "not set";
494
1121
  const activeModel = ctx.model;
495
1122
  const isAR = isAgentRouter(activeModel?.provider, (activeModel as any)?.baseUrl);
496
1123
  const order = getPackageOrderState();
@@ -506,16 +1133,18 @@ export default function (pi: ExtensionAPI) {
506
1133
  }
507
1134
 
508
1135
  ctx.ui.notify(
509
- `[AgentRouter Plugin]\n` +
510
- `- Active model: ${activeModel?.id || "none"} (${isAR ? "AgentRouter [yes]" : "Other Provider"})\n` +
511
- `- Package Priority: ${priorityStatus}\n` +
512
- `- API Key: ${maskedKey}\n` +
513
- `- Caching: Enabled (Prompt Cache + Session Affinity + Adaptive Thinking)\n` +
514
- `- Pacing Interval: ${minIntervalMs} ms (Measured from turn completion)\n` +
515
- `- Commands:\n` +
516
- ` /agentrouter key <key> (update API key)\n` +
517
- ` /agentrouter pacing <ms> (set request delay after completion)\n` +
518
- ` /agentrouter fix-order (move plugin above pi-cache-optimizer in settings.json)`,
1136
+ `[AgentRouter Plugin v2.1.1]\n` +
1137
+ `- Active model: ${activeModel?.id || "none"} (${isAR ? "AgentRouter [yes]" : "Other Provider"})\n` +
1138
+ `- Package Priority: ${priorityStatus}\n` +
1139
+ `- API Key: ${maskedKey}\n` +
1140
+ `- Caching & Pacing: Enabled (${minIntervalMs} ms delay)\n` +
1141
+ `- Commands:\n` +
1142
+ ` /agentrouter check (probe live batch quotas & spending)\n` +
1143
+ ` /agentrouter pricing (fetch live pricing table $/1M)\n` +
1144
+ ` /agentrouter sync (sync enabledModels in settings.json)\n` +
1145
+ ` /agentrouter key <key> (update API key)\n` +
1146
+ ` /agentrouter pacing <ms> (adjust rate limit delay)\n` +
1147
+ ` /agentrouter fix-order (move plugin above pi-cache-optimizer)`,
519
1148
  "info"
520
1149
  );
521
1150
  },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@madgagarin/pi-agentrouter",
3
- "version": "1.4.0",
4
- "description": "Pi coding agent extension for AgentRouter (GPT-5.6 Sol, Claude Opus 5, DeepSeek V4F) with rate-limit pacing, prompt caching, and WAF protection.",
3
+ "version": "2.1.1",
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"
7
7
  },
@@ -11,19 +11,27 @@
11
11
  "pi-coding-agent",
12
12
  "pi",
13
13
  "agentrouter",
14
- "agentrouter-org",
15
14
  "agentrouter.org",
15
+ "agentrouter-org",
16
16
  "agent-router",
17
+ "pi-agentrouter",
18
+ "gpt-6-astra",
19
+ "gpt-6",
17
20
  "gpt-5.6-sol",
18
21
  "claude-opus-5",
19
22
  "claude-opus-4-8",
23
+ "deepseek-v4-flash",
20
24
  "deepseek-v4f",
25
+ "glm-5.3",
21
26
  "deepseek",
22
27
  "prompt-caching",
23
28
  "openai",
24
29
  "claude",
30
+ "anthropic",
25
31
  "llm-gateway",
26
- "ai-coding"
32
+ "llm-router",
33
+ "ai-coding",
34
+ "coding-agent"
27
35
  ],
28
36
  "author": "madgagarin (https://github.com/madgagarin)",
29
37
  "license": "MIT",