@madgagarin/pi-agentrouter 1.3.0 β†’ 2.0.0

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 +78 -78
  2. package/index.ts +587 -112
  3. package/package.json +12 -4
package/README.md CHANGED
@@ -1,130 +1,130 @@
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
+ [![AgentRouter Gateway](https://img.shields.io/badge/Gateway-agentrouter.org-orange.svg)](https://agentrouter.org)
6
7
 
7
- A native, zero-config extension for the [Pi Coding Agent](https://pi.dev) that connects and optimizes [AgentRouter](https://agentrouter.org) models (GPT-5.6 Sol, Claude Opus 4.8 / 5) with built-in WAF rate-limit pacing, client fingerprint preservation, prompt caching, and seamless compaction.
8
+ Use **GPT-5.6 Sol**, **Claude Opus 5**, **Claude Opus 4.8**, **DeepSeek V4 Flash**, and **GLM 5.3** 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-5.6, Claude Opus 5, and DeepSeek V4 β€” no credit card needed. That's enough for **over 80,000,000 tokens** on DeepSeek V4!
11
+ > πŸ‘‰ **[Claim your free trial credits on AgentRouter.org β†’](https://agentrouter.org/register?aff=34dc)**
8
12
 
9
13
  ---
10
14
 
11
- ## 🎁 Free Credits & Author Referral Bonus
15
+ ## Quick Start
16
+
17
+ ### 1. Get your API key
12
18
 
13
- 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.
14
20
 
15
- * **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 and Claude Opus models.
16
- * **Support the Author:** Using this link directly supports the author and the ongoing maintenance of this open-source plugin!
21
+ ### 2. Install the extension
17
22
 
18
- πŸ‘‰ **[Sign up on AgentRouter (Referral Link with Bonus)](https://agentrouter.org/register?aff=34dc)** πŸ‘ˆ
23
+ ```bash
24
+ pi install npm:@madgagarin/pi-agentrouter
25
+ ```
19
26
 
20
- *(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).*
21
34
 
22
35
  ---
23
36
 
24
- ## ✨ Features
37
+ ## Why use this plugin?
38
+
39
+ [AgentRouter](https://agentrouter.org) ([agentrouter.org](https://agentrouter.org)) provides affordable unified access to frontier LLMs, but using raw OpenAI/Anthropic proxy configurations in Pi often runs into edge cases: Cloudflare WAF checks, rate-limit bursts from parallel subagents, prompt caching cache misses, and role naming conflicts.
25
40
 
26
- - πŸ”‘ **Unified API Key**: Use a single `sk-...` (or custom token) for all modelsβ€”both OpenAI GPT and Anthropic Claude endpoints are authenticated seamlessly.
27
- - πŸš€ **Zero-Config Model Auto-Registration**: Automatically registers:
28
- - `agentrouter-openai/gpt-5.6-sol` (1M Context Window, Native Reasoning, Session Affinity).
29
- - `agentrouter-clode/claude-opus-4-8` (512K Context Window, Adaptive Thinking).
30
- - `agentrouter-clode/claude-opus-5` (1M Context Window, Adaptive Thinking).
31
- - ⚑ **Optimized Prompt Caching**: Bundles and configures session affinity headers and thinking structures, achieving **>80% cache hit rates** on consecutive turns.
32
- - πŸ›‘οΈ **WAF & Rate-Limit Pacing**: Smart 2.5s throttling applied specifically to AgentRouter calls to avoid cloud WAF / 405 rate-limit blocks.
33
- - πŸ“¦ **Seamless Compaction (Fixes 401)**: Solves the `401 unauthorized client` error during `/compact` by ensuring valid Pi client fingerprint headers are passed during summarization.
34
- - ⌨️ **Interactive Terminal Controls**: Change API keys, adjust throttle pacing, and cycle through models or thinking depths directly in the TUI.
41
+ This plugin fixes all of that automatically:
42
+
43
+ - **Live USD Pricing in Pi:** Pulls current rates from [agentrouter.org/api/pricing](https://agentrouter.org/api/pricing) on startup so Pi's built-in cost tracking shows your exact spend in dollars.
44
+ - **Live Quota Monitor (`/agentrouter check`):** Quick health check that pings all models to see if daily batch quotas are open, and displays your monthly usage in USD.
45
+ - **1M Context Windows:** All models are configured with their full 1,048,576 token context limits and native reasoning/adaptive thinking.
46
+ - **Cross-Process Rate Pacing:** Uses a shared lock file (`~/.pi/agent/.agentrouter-pacing`) so background subagents (`pi-subagents`) and the main chat won't trip 429 rate limits.
47
+ - **Zero 400 & 401 Errors:** Handles canonical `pi-code` prompt header placement for WAF authorization and automatically normalizes OpenAI `developer` roles to `system`.
48
+ - **High Cache Hit Rates (>80%):** Preserves session affinity headers and disables destructive prompt rewriting on AgentRouter routes.
35
49
 
36
50
  ---
37
51
 
38
- ## πŸš€ Installation
52
+ ## Models & Pricing
39
53
 
40
- ### Option 1: Install via npm (Recommended)
41
- ```bash
42
- pi install npm:@madgagarin/pi-agentrouter
43
- ```
54
+ Rates are pulled directly from the [agentrouter.org](https://agentrouter.org) gateway API ($2.00 / 1M tokens base unit):
44
55
 
45
- ### Option 2: Install directly from GitHub
46
- ```bash
47
- pi install git:github.com/madgagarin/pi-agentrouter
48
- ```
56
+ | Model | Provider | Context | Output | Reasoning | Input / 1M | Output / 1M | Quota Policy |
57
+ | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
58
+ | `deepseek-v4-flash` | `agentrouter-openai` | 1M | 64K | Yes | $2.00 | $6.00 | Unlimited |
59
+ | `glm-5.3` | `agentrouter-openai` | 1M | 128K | Yes | $3.00 | $12.00 | Unlimited |
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) | $8.00 | $40.00 | Daily batch drops |
62
+ | `claude-opus-4-8` | `agentrouter-clode` | 1M | 64K | Yes (Adaptive) | $8.00 | $40.00 | Daily batch drops |
49
63
 
50
- ### Option 3: Local Installation
51
- ```bash
52
- pi install /path/to/pi-agentrouter
53
- ```
64
+ *Note: Claude and GPT models are released in daily batches on AgentRouter. If you hit a 402, run `/agentrouter check` to verify, and switch to `deepseek-v4-flash` or `glm-5.3` for unlimited coding.*
54
65
 
55
66
  ---
56
67
 
57
- ## βš™οΈ Configuration
68
+ ## In-Chat Commands
58
69
 
59
- ### 1. Set Your API Key
70
+ | Command | What it does |
71
+ | :--- | :--- |
72
+ | `/agentrouter` | Overview of active model, current monthly USD spend, package order, and pacing delay. |
73
+ | `/agentrouter check` | Live preflight probe of all model quotas (200 OK vs 402) and monthly usage. |
74
+ | `/agentrouter pricing` | Fetches and prints the latest official pricing table from [agentrouter.org](https://agentrouter.org). |
75
+ | `/agentrouter key <key>` | Sets API key and syncs it across `agentrouter.json` and Pi's `auth.json`. |
76
+ | `/agentrouter pacing <ms>` | Sets delay between consecutive requests (default: `3500` ms). |
77
+ | `/agentrouter fix-order` | Places this plugin above `pi-cache-optimizer` in `settings.json` if needed. |
78
+ | `/compact` | Compresses chat history safely without 401 authorization drops. |
60
79
 
61
- **Option A: Inside Pi Chat (Easiest)**
62
- Simply set it directly inside your interactive Pi chat session:
63
- ```text
64
- /agentrouter key sk-your-agentrouter-key
65
- ```
80
+ ---
66
81
 
67
- **Option B: Environment Variable**
68
- Alternatively, export the environment variable in your `~/.bashrc` or `~/.zshrc`:
69
- ```bash
70
- export AGENTROUTER_API_KEY="sk-your-agentrouter-key"
71
- ```
82
+ ## Keybindings
83
+
84
+ | Shortcut | Action |
85
+ | :--- | :--- |
86
+ | `Ctrl + P` | Cycle to next model (`deepseek-v4-flash` βž” `glm-5.3` βž” `gpt-5.6-sol` βž” `claude-opus-5` βž” `claude-opus-4-8`) |
87
+ | `Shift + Ctrl + P` | Cycle to previous model |
88
+ | `Shift + Tab` | Toggle reasoning depth (`off` βž” `minimal` βž” `low` βž” `medium` βž” `high`) |
89
+ | `Ctrl + T` | Toggle reasoning block visibility |
90
+ | `Ctrl + L` | Fuzzy-search model picker |
72
91
 
73
92
  ---
74
93
 
75
- ### 2. Configure Model Cycling (`settings.json`)
94
+ ## Recommended `settings.json`
76
95
 
77
- To enable quick model cycling with `Ctrl+P` and set default thinking levels, add the following to `~/.pi/agent/settings.json`:
96
+ Add this to `~/.pi/agent/settings.json` for convenient model switching:
78
97
 
79
98
  ```json
80
99
  {
81
100
  "defaultProvider": "agentrouter-openai",
82
- "defaultModel": "gpt-5.6-sol",
83
- "defaultThinkingLevel": "medium",
101
+ "defaultModel": "deepseek-v4-flash",
102
+ "defaultThinkingLevel": "low",
84
103
  "enabledModels": [
104
+ "agentrouter-openai/deepseek-v4-flash",
105
+ "agentrouter-openai/glm-5.3",
85
106
  "agentrouter-openai/gpt-5.6-sol",
86
- "agentrouter-clode/claude-opus-4-8",
87
- "agentrouter-clode/claude-opus-5"
107
+ "agentrouter-clode/claude-opus-5",
108
+ "agentrouter-clode/claude-opus-4-8"
88
109
  ]
89
110
  }
90
111
  ```
91
112
 
92
113
  ---
93
114
 
94
- ## ⌨️ Hotkeys & Shortcuts
95
-
96
- | Shortcut | Action | Description |
97
- | :--- | :--- | :--- |
98
- | `Ctrl + P` | πŸ”„ **Next Model** | Cycles to the next model (`gpt-5.6-sol` βž” `claude-opus-4-8` βž” `claude-opus-5`). |
99
- | `Shift + Ctrl + P` | πŸ”„ **Previous Model** | Cycles to the previous model. |
100
- | `Shift + Tab` | 🧠 **Cycle Thinking Level** | Toggles reasoning depth: `off` βž” `minimal` βž” `low` βž” `medium` βž” `high`. |
101
- | `Ctrl + T` | πŸ‘ **Toggle Thinking Visibility** | Collapses or expands thinking/reasoning blocks on screen. |
102
- | `Ctrl + L` | πŸ“‹ **Model Selector** | Opens interactive fuzzy-search model picker. |
103
-
104
- ---
105
-
106
- ## πŸ› οΈ In-Chat Commands
107
-
108
- * `/agentrouter` β€” View current plugin status, active model, masked key, and pacing interval.
109
- * `/agentrouter key <your-key>` β€” Update API key for all AgentRouter models on the fly.
110
- * `/agentrouter pacing <ms>` β€” Adjust the minimum delay between consecutive requests (default: `2500` ms).
111
- * `/compact` β€” Compress conversation history safely without 401 authorization errors.
112
-
113
- ---
114
-
115
- ## ❓ FAQ & Troubleshooting
115
+ ## Notes & FAQ
116
116
 
117
- #### Q: Why does `/compact` fail on raw proxy configurations?
118
- 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.
117
+ #### How does quota batching work on Claude / GPT?
118
+ 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.
119
119
 
120
- #### Q: Does the 2.5s pacing delay affect local or other cloud models?
121
- No. The pacing logic specifically filters for AgentRouter endpoints (`isAgentRouter`). Native OpenAI, Anthropic, Gemini, or local models run at full speed without delay.
120
+ #### Does pacing affect other models?
121
+ No. Request pacing only applies when talking to `agentrouter.org` endpoints. Local models or direct OpenAI/Google providers run at full speed.
122
122
 
123
- #### Q: How to use custom subagents with AgentRouter?
124
- 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.
123
+ #### Using custom subagents (`pi-subagents`)
124
+ 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`.
125
125
 
126
126
  ---
127
127
 
128
- ## πŸ“„ License
128
+ ## License
129
129
 
130
130
  MIT Β© [madgagarin](https://github.com/madgagarin)
package/index.ts CHANGED
@@ -5,16 +5,175 @@ 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
+ "deepseek-v4-flash": {
45
+ id: "deepseek-v4-flash",
46
+ name: "deepseek-v4-flash",
47
+ providerType: "openai",
48
+ contextWindow: 1048576,
49
+ maxTokens: 65536,
50
+ reasoning: true,
51
+ compat: { sendSessionAffinityHeaders: true },
52
+ cost: { input: 2.0 / 1_000_000, output: 6.0 / 1_000_000, cacheRead: 0, cacheWrite: 0 },
53
+ },
54
+ "deepseek-v4f": {
55
+ id: "deepseek-v4f",
56
+ name: "deepseek-v4f",
57
+ providerType: "openai",
58
+ contextWindow: 1048576,
59
+ maxTokens: 65536,
60
+ reasoning: true,
61
+ compat: { sendSessionAffinityHeaders: true },
62
+ cost: { input: 2.0 / 1_000_000, output: 6.0 / 1_000_000, cacheRead: 0, cacheWrite: 0 },
63
+ },
64
+ "glm-5.3": {
65
+ id: "glm-5.3",
66
+ name: "glm-5.3",
67
+ providerType: "openai",
68
+ contextWindow: 1048576,
69
+ maxTokens: 131072,
70
+ reasoning: true,
71
+ compat: { sendSessionAffinityHeaders: true },
72
+ cost: { input: 3.0 / 1_000_000, output: 12.0 / 1_000_000, cacheRead: 0, cacheWrite: 0 },
73
+ },
74
+ "glm-5.2": {
75
+ id: "glm-5.2",
76
+ name: "glm-5.2",
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
+ "gpt-5.6-sol": {
85
+ id: "gpt-5.6-sol",
86
+ name: "gpt-5.6-sol",
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: 15.0 / 1_000_000, cacheRead: 0, cacheWrite: 0 },
93
+ },
94
+ "gpt-5.5": {
95
+ id: "gpt-5.5",
96
+ name: "gpt-5.5",
97
+ providerType: "openai",
98
+ contextWindow: 1048576,
99
+ maxTokens: 131072,
100
+ reasoning: true,
101
+ compat: { sendSessionAffinityHeaders: true },
102
+ cost: { input: 4.0 / 1_000_000, output: 8.0 / 1_000_000, cacheRead: 0, cacheWrite: 0 },
103
+ },
104
+ "claude-opus-4-8": {
105
+ id: "claude-opus-4-8",
106
+ name: "claude-opus-4-8",
107
+ providerType: "anthropic",
108
+ contextWindow: 1048576,
109
+ maxTokens: 65536,
110
+ reasoning: true,
111
+ compat: {
112
+ forceAdaptiveThinking: true,
113
+ allowEmptySignature: true,
114
+ sendSessionAffinityHeaders: true,
115
+ supportsEagerToolInputStreaming: false,
116
+ },
117
+ cost: { input: 8.0 / 1_000_000, output: 40.0 / 1_000_000, cacheRead: 0, cacheWrite: 0 },
118
+ },
119
+ "claude-opus-5": {
120
+ id: "claude-opus-5",
121
+ name: "claude-opus-5",
122
+ providerType: "anthropic",
123
+ contextWindow: 1048576,
124
+ maxTokens: 65536,
125
+ reasoning: true,
126
+ compat: {
127
+ forceAdaptiveThinking: true,
128
+ allowEmptySignature: true,
129
+ sendSessionAffinityHeaders: true,
130
+ supportsEagerToolInputStreaming: false,
131
+ },
132
+ cost: { input: 8.0 / 1_000_000, output: 40.0 / 1_000_000, cacheRead: 0, cacheWrite: 0 },
133
+ },
134
+ "claude-opus-4-7": {
135
+ id: "claude-opus-4-7",
136
+ name: "claude-opus-4-7",
137
+ providerType: "anthropic",
138
+ contextWindow: 1048576,
139
+ maxTokens: 65536,
140
+ reasoning: true,
141
+ compat: {
142
+ forceAdaptiveThinking: true,
143
+ allowEmptySignature: true,
144
+ sendSessionAffinityHeaders: true,
145
+ supportsEagerToolInputStreaming: false,
146
+ },
147
+ cost: { input: 8.0 / 1_000_000, output: 40.0 / 1_000_000, cacheRead: 0, cacheWrite: 0 },
148
+ },
149
+ "claude-opus-4-6": {
150
+ id: "claude-opus-4-6",
151
+ name: "claude-opus-4-6",
152
+ providerType: "anthropic",
153
+ contextWindow: 1048576,
154
+ maxTokens: 65536,
155
+ reasoning: true,
156
+ compat: {
157
+ forceAdaptiveThinking: true,
158
+ allowEmptySignature: true,
159
+ sendSessionAffinityHeaders: true,
160
+ supportsEagerToolInputStreaming: false,
161
+ },
162
+ cost: { input: 2.0 / 1_000_000, output: 10.0 / 1_000_000, cacheRead: 0, cacheWrite: 0 },
163
+ },
164
+ };
165
+
14
166
  export function loadConfig(): AgentRouterConfig {
15
167
  try {
16
168
  if (fs.existsSync(CONFIG_FILE)) {
17
- return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
169
+ const cfg = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
170
+ if (cfg.apiKey) return cfg;
171
+ }
172
+ const authPath = path.join(process.env.HOME || "", ".pi/agent/auth.json");
173
+ if (fs.existsSync(authPath)) {
174
+ const auth = JSON.parse(fs.readFileSync(authPath, "utf-8"));
175
+ const key = auth["agentrouter-openai"]?.key || auth["agentrouter-clode"]?.key;
176
+ if (key) return { apiKey: key };
18
177
  }
19
178
  } catch {}
20
179
  return {};
@@ -27,6 +186,20 @@ export function saveConfig(cfg: AgentRouterConfig): void {
27
186
  fs.mkdirSync(dir, { recursive: true });
28
187
  }
29
188
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2), "utf-8");
189
+
190
+ if (cfg.apiKey) {
191
+ const authPath = path.join(process.env.HOME || "", ".pi/agent/auth.json");
192
+ let auth: Record<string, any> = {};
193
+ if (fs.existsSync(authPath)) {
194
+ try {
195
+ auth = JSON.parse(fs.readFileSync(authPath, "utf-8"));
196
+ } catch {}
197
+ }
198
+ auth["agentrouter-openai"] = { type: "api_key", key: cfg.apiKey };
199
+ auth["agentrouter-clode"] = { type: "api_key", key: cfg.apiKey };
200
+ auth["anthropic"] = { type: "api_key", key: cfg.apiKey };
201
+ fs.writeFileSync(authPath, JSON.stringify(auth, null, 2), "utf-8");
202
+ }
30
203
  } catch {}
31
204
  }
32
205
 
@@ -35,13 +208,29 @@ export function normalizeApiKey(key?: string): string {
35
208
  return key.trim().replace(/^["']|["']$/g, "").trim();
36
209
  }
37
210
 
38
- export interface PackageOrderState {
39
- agentRouterIndex: number;
40
- cacheOptimizerIndex: number;
41
- needsFix: boolean;
211
+ export function loadCachedPricing(): ApiPricingModel[] | null {
212
+ try {
213
+ if (fs.existsSync(MODELS_CACHE_FILE)) {
214
+ const data = JSON.parse(fs.readFileSync(MODELS_CACHE_FILE, "utf-8"));
215
+ if (Array.isArray(data) && data.length > 0) {
216
+ return data;
217
+ }
218
+ }
219
+ } catch {}
220
+ return null;
221
+ }
222
+
223
+ export function saveCachedPricing(models: ApiPricingModel[]): void {
224
+ try {
225
+ const dir = path.dirname(MODELS_CACHE_FILE);
226
+ if (!fs.existsSync(dir)) {
227
+ fs.mkdirSync(dir, { recursive: true });
228
+ }
229
+ fs.writeFileSync(MODELS_CACHE_FILE, JSON.stringify(models, null, 2), "utf-8");
230
+ } catch {}
42
231
  }
43
232
 
44
- export function getPackageOrderState(): PackageOrderState {
233
+ export function getPackageOrderState(): { agentRouterIndex: number; cacheOptimizerIndex: number; needsFix: boolean } {
45
234
  try {
46
235
  if (!fs.existsSync(SETTINGS_FILE)) {
47
236
  return { agentRouterIndex: -1, cacheOptimizerIndex: -1, needsFix: false };
@@ -50,12 +239,10 @@ export function getPackageOrderState(): PackageOrderState {
50
239
  if (!Array.isArray(settings.packages)) {
51
240
  return { agentRouterIndex: -1, cacheOptimizerIndex: -1, needsFix: false };
52
241
  }
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")
242
+ const arIdx = settings.packages.findIndex(
243
+ (p: string) => typeof p === "string" && (p.includes("@madgagarin/pi-agentrouter") || p.includes("pi-agentrouter"))
58
244
  );
245
+ const cacheIdx = settings.packages.findIndex((p: string) => typeof p === "string" && p.includes("pi-cache-optimizer"));
59
246
  const needsFix = cacheIdx !== -1 && arIdx !== -1 && arIdx > cacheIdx;
60
247
  return { agentRouterIndex: arIdx, cacheOptimizerIndex: cacheIdx, needsFix };
61
248
  } catch {
@@ -68,16 +255,14 @@ export function fixPackagePriorityInSettings(): boolean {
68
255
  if (!fs.existsSync(SETTINGS_FILE)) return false;
69
256
  const settings = JSON.parse(fs.readFileSync(SETTINGS_FILE, "utf-8"));
70
257
  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")
258
+ const arIdx = settings.packages.findIndex(
259
+ (p: string) => typeof p === "string" && (p.includes("@madgagarin/pi-agentrouter") || p.includes("pi-agentrouter"))
76
260
  );
261
+ const cacheIdx = settings.packages.findIndex((p: string) => typeof p === "string" && p.includes("pi-cache-optimizer"));
77
262
  if (cacheIdx !== -1 && arIdx !== -1 && arIdx > cacheIdx) {
78
263
  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")
264
+ const targetCacheIdx = settings.packages.findIndex(
265
+ (p: string) => typeof p === "string" && p.includes("pi-cache-optimizer")
81
266
  );
82
267
  settings.packages.splice(targetCacheIdx, 0, pkg);
83
268
  fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), "utf-8");
@@ -87,11 +272,6 @@ export function fixPackagePriorityInSettings(): boolean {
87
272
  return false;
88
273
  }
89
274
 
90
- const initialConfig = loadConfig();
91
- let currentApiKey = normalizeApiKey(process.env.AGENTROUTER_API_KEY || initialConfig.apiKey || "");
92
- let minIntervalMs = initialConfig.minIntervalMs ?? 2500;
93
- const PACING_FILE = path.join(process.env.HOME || "", ".pi/agent/.agentrouter-pacing");
94
-
95
275
  export function getLastRequestEndTime(): number {
96
276
  try {
97
277
  if (fs.existsSync(PACING_FILE)) {
@@ -121,13 +301,6 @@ export function isAgentRouter(providerName?: string, baseUrl?: string): boolean
121
301
  export const CANONICAL_PI_HEADER =
122
302
  "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
303
 
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
304
  export function enforceCanonicalRootPrompt(systemPrompt: string | any[] | undefined): string | any[] {
132
305
  if (!systemPrompt) {
133
306
  return CANONICAL_PI_HEADER;
@@ -165,8 +338,212 @@ export function enforceCanonicalRootPrompt(systemPrompt: string | any[] | undefi
165
338
  return systemPrompt;
166
339
  }
167
340
 
341
+ export async function fetchLivePricing(): Promise<ApiPricingModel[] | null> {
342
+ try {
343
+ const res = await fetch("https://agentrouter.org/api/pricing", {
344
+ headers: { "User-Agent": "pi-code" },
345
+ });
346
+ if (!res.ok) return null;
347
+ const data = await res.json();
348
+ if (data && Array.isArray(data.data) && data.data.length > 0) {
349
+ return data.data;
350
+ }
351
+ } catch {}
352
+ return null;
353
+ }
354
+
355
+ export async function fetchTokenUsage(apiKey: string): Promise<number | null> {
356
+ if (!apiKey) return null;
357
+ try {
358
+ const now = new Date();
359
+ const year = now.getFullYear();
360
+ const month = String(now.getMonth() + 1).padStart(2, "0");
361
+ const startDate = `${year}-${month}-01`;
362
+ const endDate = `${year}-${month}-31`;
363
+ const res = await fetch(
364
+ `https://agentrouter.org/v1/dashboard/billing/usage?start_date=${startDate}&end_date=${endDate}`,
365
+ {
366
+ headers: {
367
+ Authorization: `Bearer ${apiKey}`,
368
+ "User-Agent": "pi-code",
369
+ },
370
+ }
371
+ );
372
+ if (!res.ok) return null;
373
+ const data = await res.json();
374
+ if (data && typeof data.total_usage === "number") {
375
+ return data.total_usage;
376
+ }
377
+ } catch {}
378
+ return null;
379
+ }
380
+
381
+ export interface ModelProbeResult {
382
+ model: string;
383
+ status: "READY" | "QUOTA_EXHAUSTED" | "FORBIDDEN" | "ERROR";
384
+ code: number;
385
+ message?: string;
386
+ }
387
+
388
+ export async function probeModelQuota(modelId: string, apiKey: string, isAnthropic: boolean): Promise<ModelProbeResult> {
389
+ const url = isAnthropic ? "https://agentrouter.org/v1/messages" : "https://agentrouter.org/v1/chat/completions";
390
+ const headers = isAnthropic
391
+ ? {
392
+ "Content-Type": "application/json",
393
+ "x-api-key": apiKey,
394
+ "anthropic-version": "2023-06-01",
395
+ "User-Agent": "pi-code",
396
+ }
397
+ : {
398
+ "Content-Type": "application/json",
399
+ Authorization: `Bearer ${apiKey}`,
400
+ "User-Agent": "pi-code",
401
+ };
402
+
403
+ const body = isAnthropic
404
+ ? {
405
+ model: modelId,
406
+ system: CANONICAL_PI_HEADER,
407
+ messages: [{ role: "user", content: "ping" }],
408
+ max_tokens: 1,
409
+ }
410
+ : {
411
+ model: modelId,
412
+ messages: [
413
+ { role: "system", content: CANONICAL_PI_HEADER },
414
+ { role: "user", content: "ping" },
415
+ ],
416
+ max_tokens: 1,
417
+ };
418
+
419
+ try {
420
+ const res = await fetch(url, {
421
+ method: "POST",
422
+ headers,
423
+ body: JSON.stringify(body),
424
+ });
425
+
426
+ if (res.ok) {
427
+ return { model: modelId, status: "READY", code: res.status };
428
+ }
429
+
430
+ const data = await res.json().catch(() => ({}));
431
+ const msg = data.error?.message || data.message || "";
432
+
433
+ if (res.status === 402 || msg.toLowerCase().includes("quota") || msg.toLowerCase().includes("exhausted")) {
434
+ return { model: modelId, status: "QUOTA_EXHAUSTED", code: 402, message: msg };
435
+ }
436
+ if (res.status === 403) {
437
+ return { model: modelId, status: "FORBIDDEN", code: 403, message: msg };
438
+ }
439
+ return { model: modelId, status: "ERROR", code: res.status, message: msg };
440
+ } catch (err: any) {
441
+ return { model: modelId, status: "ERROR", code: 0, message: err.message };
442
+ }
443
+ }
444
+
168
445
  export default function (pi: ExtensionAPI) {
169
- function registerAgentRouterProviders(apiKey: string): void {
446
+ function getEffectiveApiKey(): string {
447
+ const cfg = loadConfig();
448
+ return normalizeApiKey(process.env.AGENTROUTER_API_KEY || process.env.AGENT_ROUTER_API_KEY || cfg.apiKey || "");
449
+ }
450
+
451
+ const initialConfig = loadConfig();
452
+ let currentApiKey = getEffectiveApiKey();
453
+ let minIntervalMs = initialConfig.minIntervalMs ?? 3500;
454
+
455
+ function buildModelsFromPricing(apiPricing: ApiPricingModel[] | null): {
456
+ openaiModels: any[];
457
+ claudeModels: any[];
458
+ newModels: string[];
459
+ } {
460
+ const openaiModels: any[] = [];
461
+ const claudeModels: any[] = [];
462
+ const newModels: string[] = [];
463
+ const processed = new Set<string>();
464
+
465
+ if (apiPricing && apiPricing.length > 0) {
466
+ for (const item of apiPricing) {
467
+ const id = item.model_name;
468
+ processed.add(id);
469
+ const spec = KNOWN_MODEL_SPECS[id];
470
+
471
+ const inCost = (item.model_ratio * 2.0) / 1_000_000;
472
+ const outCost = (item.model_ratio * item.completion_ratio * 2.0) / 1_000_000;
473
+
474
+ if (spec) {
475
+ const modelObj = {
476
+ id: spec.id,
477
+ name: spec.name,
478
+ reasoning: spec.reasoning,
479
+ input: ["text"],
480
+ contextWindow: spec.contextWindow,
481
+ maxTokens: spec.maxTokens,
482
+ cost: { input: inCost, output: outCost, cacheRead: 0, cacheWrite: 0 },
483
+ compat: spec.compat || { sendSessionAffinityHeaders: true },
484
+ };
485
+ if (spec.providerType === "anthropic") {
486
+ claudeModels.push(modelObj);
487
+ } else {
488
+ openaiModels.push(modelObj);
489
+ }
490
+ } else {
491
+ newModels.push(id);
492
+ const isAnthropic =
493
+ item.supported_endpoint_types.includes("anthropic") && !item.supported_endpoint_types.includes("openai");
494
+ const modelObj = {
495
+ id,
496
+ name: id,
497
+ reasoning: true,
498
+ input: ["text"],
499
+ contextWindow: 131072,
500
+ maxTokens: 16384,
501
+ cost: { input: inCost, output: outCost, cacheRead: 0, cacheWrite: 0 },
502
+ compat: isAnthropic
503
+ ? {
504
+ forceAdaptiveThinking: true,
505
+ allowEmptySignature: true,
506
+ sendSessionAffinityHeaders: true,
507
+ }
508
+ : {
509
+ sendSessionAffinityHeaders: true,
510
+ },
511
+ };
512
+ if (isAnthropic) {
513
+ claudeModels.push(modelObj);
514
+ } else {
515
+ openaiModels.push(modelObj);
516
+ }
517
+ }
518
+ }
519
+ }
520
+
521
+ for (const [id, spec] of Object.entries(KNOWN_MODEL_SPECS)) {
522
+ if (!processed.has(id)) {
523
+ const modelObj = {
524
+ id: spec.id,
525
+ name: spec.name,
526
+ reasoning: spec.reasoning,
527
+ input: ["text"],
528
+ contextWindow: spec.contextWindow,
529
+ maxTokens: spec.maxTokens,
530
+ cost: spec.cost || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
531
+ compat: spec.compat || { sendSessionAffinityHeaders: true },
532
+ };
533
+ if (spec.providerType === "anthropic") {
534
+ claudeModels.push(modelObj);
535
+ } else {
536
+ openaiModels.push(modelObj);
537
+ }
538
+ }
539
+ }
540
+
541
+ return { openaiModels, claudeModels, newModels };
542
+ }
543
+
544
+ function registerAgentRouterProviders(apiKey: string, pricingData: ApiPricingModel[] | null): string[] {
545
+ const { openaiModels, claudeModels, newModels } = buildModelsFromPricing(pricingData);
546
+
170
547
  pi.registerProvider("agentrouter-openai", {
171
548
  name: "AgentRouter OpenAI",
172
549
  baseUrl: "https://agentrouter.org/v1",
@@ -175,20 +552,7 @@ export default function (pi: ExtensionAPI) {
175
552
  compat: {
176
553
  sendSessionAffinityHeaders: true,
177
554
  },
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
- ],
555
+ models: openaiModels,
192
556
  });
193
557
 
194
558
  pi.registerProvider("agentrouter-clode", {
@@ -196,43 +560,20 @@ export default function (pi: ExtensionAPI) {
196
560
  baseUrl: "https://agentrouter.org",
197
561
  apiKey,
198
562
  api: "anthropic-messages",
199
-
200
563
  compat: {
201
564
  forceAdaptiveThinking: true,
565
+ allowEmptySignature: true,
202
566
  sendSessionAffinityHeaders: true,
567
+ supportsEagerToolInputStreaming: false,
203
568
  },
204
- models: [
205
- {
206
- id: "claude-opus-4-8",
207
- name: "claude-opus-4-8",
208
- reasoning: true,
209
- input: ["text"],
210
- contextWindow: 524288,
211
- maxTokens: 65536,
212
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
213
- compat: {
214
- forceAdaptiveThinking: true,
215
- sendSessionAffinityHeaders: true,
216
- },
217
- },
218
- {
219
- id: "claude-opus-5",
220
- name: "claude-opus-5",
221
- reasoning: true,
222
- input: ["text"],
223
- contextWindow: 1048576,
224
- maxTokens: 65536,
225
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
226
- compat: {
227
- forceAdaptiveThinking: true,
228
- sendSessionAffinityHeaders: true,
229
- },
230
- },
231
- ],
569
+ models: claudeModels,
232
570
  });
571
+
572
+ return newModels;
233
573
  }
234
574
 
235
- registerAgentRouterProviders(currentApiKey);
575
+ const cachedPricing = loadCachedPricing();
576
+ registerAgentRouterProviders(currentApiKey, cachedPricing);
236
577
 
237
578
  function updatePromptRewriteEnvForModel(model?: any): void {
238
579
  if (isAgentRouter(model?.provider, model?.baseUrl)) {
@@ -242,23 +583,33 @@ export default function (pi: ExtensionAPI) {
242
583
  }
243
584
  }
244
585
 
245
- // Enforce root system prompt signature and prompt prefix stability at the lowest transport level
246
586
  pi.on("before_provider_request", async (event, ctx) => {
247
587
  const provider = ((event as any)?.model?.provider || ctx?.model?.provider || "").toLowerCase();
248
- const baseUrl = ((event as any)?.model?.baseUrl || (ctx?.model as any)?.baseUrl || "");
588
+ const baseUrl = (event as any)?.model?.baseUrl || (ctx?.model as any)?.baseUrl || "";
249
589
 
250
590
  if (isAgentRouter(provider, baseUrl)) {
591
+ const lastEnd = getLastRequestEndTime();
592
+ const now = Date.now();
593
+ const elapsed = now - lastEnd;
594
+ if (lastEnd > 0 && elapsed < minIntervalMs) {
595
+ const waitMs = minIntervalMs - elapsed;
596
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
597
+ }
598
+
251
599
  const payload = event.payload;
252
600
  if (payload) {
253
- // 1. Enforce canonical root system prompt in payload.system (Anthropic format)
254
601
  if (payload.system !== undefined) {
255
602
  payload.system = enforceCanonicalRootPrompt(payload.system);
256
603
  }
257
-
258
- // 2. Enforce canonical root system prompt in payload.messages[0] (OpenAI format)
259
604
  if (Array.isArray(payload.messages) && payload.messages.length > 0) {
605
+ for (const msg of payload.messages) {
606
+ if (msg && msg.role === "developer") {
607
+ msg.role = "system";
608
+ }
609
+ }
260
610
  const firstMsg = payload.messages[0];
261
- if (firstMsg && firstMsg.role === "system") {
611
+ if (firstMsg && (firstMsg.role === "system" || firstMsg.role === "developer")) {
612
+ firstMsg.role = "system";
262
613
  if (typeof firstMsg.content === "string") {
263
614
  firstMsg.content = enforceCanonicalRootPrompt(firstMsg.content);
264
615
  } else if (Array.isArray(firstMsg.content)) {
@@ -268,25 +619,65 @@ export default function (pi: ExtensionAPI) {
268
619
  }
269
620
  }
270
621
  }
622
+ return undefined;
623
+ });
624
+
625
+ pi.on("message_end", async (_event, ctx) => {
626
+ const model = ctx?.model;
627
+ if (isAgentRouter(model?.provider, (model as any)?.baseUrl)) {
628
+ setLastRequestEndTime(Date.now());
629
+ }
630
+ });
631
+
632
+ pi.on("turn_end", async (_event, ctx) => {
633
+ const model = ctx?.model;
634
+ if (isAgentRouter(model?.provider, (model as any)?.baseUrl)) {
635
+ setLastRequestEndTime(Date.now());
636
+ }
637
+ });
638
+
639
+ pi.on("agent_end", async (_event, ctx) => {
640
+ const model = ctx?.model;
641
+ if (isAgentRouter(model?.provider, (model as any)?.baseUrl)) {
642
+ setLastRequestEndTime(Date.now());
643
+ }
271
644
  });
272
645
 
273
646
  pi.on("session_start", async (_event, ctx) => {
647
+ currentApiKey = getEffectiveApiKey();
274
648
  updatePromptRewriteEnvForModel(ctx.model);
275
649
  setLastRequestEndTime(Date.now());
276
650
 
651
+ fetchLivePricing().then((livePricing) => {
652
+ if (livePricing) {
653
+ saveCachedPricing(livePricing);
654
+ const newModels = registerAgentRouterProviders(currentApiKey, livePricing);
655
+ if (newModels.length > 0 && ctx.hasUI) {
656
+ ctx.ui.notify(
657
+ `[AgentRouter] Discovered new models on gateway: ${newModels.join(", ")}.\n` +
658
+ `Auto-registered with safe default limits (128K context). Check settings.json or await plugin update for optimized specs.`,
659
+ "info"
660
+ );
661
+ }
662
+ }
663
+ });
664
+
277
665
  const order = getPackageOrderState();
278
666
  if (order.needsFix && ctx.hasUI && typeof (ctx.ui as any).confirm === "function") {
279
667
  try {
280
668
  const confirmed = await (ctx.ui as any).confirm(
281
669
  "Pi AgentRouter Package Priority",
282
670
  "@madgagarin/pi-agentrouter is listed AFTER pi-cache-optimizer in settings.json packages.\n\n" +
283
- "It must be placed before pi-cache-optimizer so prompt cache bypass takes effect before cache-optimizer transforms the prompt.\n\n" +
284
- "Move @madgagarin/pi-agentrouter directly above pi-cache-optimizer in settings.json?"
671
+ "It must be placed before pi-cache-optimizer so prompt cache bypass takes effect before cache-optimizer transforms the prompt.\n\n" +
672
+ "Move @madgagarin/pi-agentrouter directly above pi-cache-optimizer in settings.json?"
285
673
  );
286
674
  if (confirmed) {
287
675
  const success = fixPackagePriorityInSettings();
288
676
  if (success) {
289
- ctx.ui.notify("@madgagarin/pi-agentrouter moved above pi-cache-optimizer in settings.json. Please restart Pi for changes to take full effect.", "info");
677
+ ctx.ui.notify(
678
+ "@madgagarin/pi-agentrouter moved above pi-cache-optimizer in settings.json. Please restart Pi for changes to take full effect.",
679
+ "info"
680
+ );
290
681
  }
291
682
  }
292
683
  } catch {}
@@ -318,20 +709,6 @@ export default function (pi: ExtensionAPI) {
318
709
  }
319
710
  });
320
711
 
321
- pi.on("turn_end", async (_event, ctx) => {
322
- const model = ctx.model;
323
- if (isAgentRouter(model?.provider, (model as any)?.baseUrl)) {
324
- setLastRequestEndTime(Date.now());
325
- }
326
- });
327
-
328
- pi.on("agent_end", async (_event, ctx) => {
329
- const model = ctx.model;
330
- if (isAgentRouter(model?.provider, (model as any)?.baseUrl)) {
331
- setLastRequestEndTime(Date.now());
332
- }
333
- });
334
-
335
712
  pi.on("session_before_compact", async (event, ctx) => {
336
713
  const model = ctx.model;
337
714
  if (!model || !isAgentRouter(model.provider, (model as any).baseUrl)) {
@@ -406,7 +783,7 @@ export default function (pi: ExtensionAPI) {
406
783
  });
407
784
 
408
785
  pi.registerCommand("agentrouter", {
409
- description: "Manage AgentRouter plugin settings (status, API key, pacing interval)",
786
+ description: "Manage AgentRouter plugin settings, live quotas, and pricing (/agentrouter check, /agentrouter pricing)",
410
787
  handler: async (args, ctx) => {
411
788
  const parts = (args || "").trim().split(/\s+/);
412
789
  const action = parts[0]?.toLowerCase();
@@ -420,7 +797,7 @@ export default function (pi: ExtensionAPI) {
420
797
  }
421
798
  currentApiKey = cleanKey;
422
799
  saveConfig({ apiKey: cleanKey, minIntervalMs });
423
- registerAgentRouterProviders(cleanKey);
800
+ registerAgentRouterProviders(cleanKey, loadCachedPricing());
424
801
  ctx.ui.notify("AgentRouter API key updated successfully for all models.", "info");
425
802
  return;
426
803
  }
@@ -439,7 +816,10 @@ export default function (pi: ExtensionAPI) {
439
816
  }
440
817
  const success = fixPackagePriorityInSettings();
441
818
  if (success) {
442
- 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");
819
+ ctx.ui.notify(
820
+ "@madgagarin/pi-agentrouter moved directly above pi-cache-optimizer in settings.json. Please restart Pi for changes to take full effect.",
821
+ "info"
822
+ );
443
823
  } else {
444
824
  ctx.ui.notify("Failed to update settings.json.", "error");
445
825
  }
@@ -458,7 +838,101 @@ export default function (pi: ExtensionAPI) {
458
838
  return;
459
839
  }
460
840
 
461
- const maskedKey = currentApiKey.length > 8 ? `${currentApiKey.slice(0, 7)}...${currentApiKey.slice(-4)}` : "not set";
841
+ if (action === "pricing" || action === "price" || action === "models") {
842
+ ctx.ui.notify("Fetching live pricing from AgentRouter API...", "info");
843
+ const live = await fetchLivePricing();
844
+ if (live) {
845
+ saveCachedPricing(live);
846
+ registerAgentRouterProviders(currentApiKey, live);
847
+ }
848
+ const pricing = live || loadCachedPricing() || [];
849
+ if (pricing.length === 0) {
850
+ ctx.ui.notify("Unable to fetch pricing from AgentRouter API. Check internet connection.", "error");
851
+ return;
852
+ }
853
+
854
+ let tableText =
855
+ `[AgentRouter Official Pricing]\n` +
856
+ `Live rates calculated from AgentRouter gateway API (1.0x ratio = $2.00/1M tokens):\n\n` +
857
+ `Model Input / 1M Output / 1M Ratio (In/Out) Protocol\n` +
858
+ `------------------------------------------------------------------------\n`;
859
+
860
+ for (const item of pricing) {
861
+ const inPrice = `$${(item.model_ratio * 2.0).toFixed(2)}`;
862
+ const outPrice = `$${(item.model_ratio * item.completion_ratio * 2.0).toFixed(2)}`;
863
+ const ratio = `${item.model_ratio.toFixed(1)}x / ${item.completion_ratio.toFixed(1)}x`;
864
+ const proto = item.supported_endpoint_types.join(", ");
865
+ tableText += `${item.model_name.padEnd(21)} ${inPrice.padEnd(12)} ${outPrice.padEnd(13)} ${ratio.padEnd(16)} ${proto}\n`;
866
+ }
867
+
868
+ tableText +=
869
+ `------------------------------------------------------------------------\n` +
870
+ `* Output cost = Input ratio Γ— completion ratio Γ— $2.00. Zero hidden fees.`;
871
+
872
+ ctx.ui.notify(tableText, "info");
873
+ return;
874
+ }
875
+
876
+ if (action === "check" || action === "quota" || action === "status-live") {
877
+ currentApiKey = getEffectiveApiKey();
878
+ if (!currentApiKey) {
879
+ ctx.ui.notify("No API key configured. Set one with /agentrouter key <your-key>", "error");
880
+ return;
881
+ }
882
+
883
+ ctx.ui.notify("Probing live model quotas and balance from AgentRouter...", "info");
884
+ const [usageUsd, pricingList] = await Promise.all([fetchTokenUsage(currentApiKey), fetchLivePricing()]);
885
+ if (pricingList) {
886
+ saveCachedPricing(pricingList);
887
+ }
888
+
889
+ const modelsToProbe = pricingList
890
+ ? pricingList.map((p) => ({
891
+ id: p.model_name,
892
+ isAnthropic: p.supported_endpoint_types.includes("anthropic") && !p.supported_endpoint_types.includes("openai"),
893
+ }))
894
+ : [
895
+ { id: "deepseek-v4-flash", isAnthropic: false },
896
+ { id: "glm-5.3", isAnthropic: false },
897
+ { id: "gpt-5.6-sol", isAnthropic: false },
898
+ { id: "claude-opus-4-8", isAnthropic: true },
899
+ { id: "claude-opus-5", isAnthropic: true },
900
+ ];
901
+
902
+ const probeResults = await Promise.all(
903
+ modelsToProbe.map((m) => probeModelQuota(m.id, currentApiKey, m.isAnthropic))
904
+ );
905
+
906
+ let report = `[AgentRouter Live Quota & Health]\n`;
907
+ if (usageUsd !== null) {
908
+ report += `- Total Spent (Current Month): $${usageUsd.toFixed(4)} USD\n\n`;
909
+ } else {
910
+ report += `- Token status: Active\n\n`;
911
+ }
912
+
913
+ report += `Model Status:\n`;
914
+ for (const res of probeResults) {
915
+ if (res.status === "READY") {
916
+ report += ` 🟒 ${res.model.padEnd(20)}: Ready (200 OK - Quota available)\n`;
917
+ } else if (res.status === "QUOTA_EXHAUSTED") {
918
+ report += ` ⏳ ${res.model.padEnd(20)}: Batch Quota Exhausted (402) - Next batch drop soon\n`;
919
+ } else if (res.status === "FORBIDDEN") {
920
+ report += ` πŸ”΄ ${res.model.padEnd(20)}: Forbidden (403) - Token has no permissions\n`;
921
+ } else {
922
+ report += ` ⚠️ ${res.model.padEnd(20)}: Error (${res.code}) ${res.message ? "- " + res.message : ""}\n`;
923
+ }
924
+ }
925
+
926
+ report +=
927
+ `\nTip: Claude and GPT models use daily batch quotas. If exhausted, switch to DeepSeek V4 or GLM 5.3 which have unlimited availability.`;
928
+
929
+ ctx.ui.notify(report, "info");
930
+ return;
931
+ }
932
+
933
+ currentApiKey = getEffectiveApiKey();
934
+ const maskedKey =
935
+ currentApiKey.length > 8 ? `${currentApiKey.slice(0, 7)}...${currentApiKey.slice(-4)}` : "not set";
462
936
  const activeModel = ctx.model;
463
937
  const isAR = isAgentRouter(activeModel?.provider, (activeModel as any)?.baseUrl);
464
938
  const order = getPackageOrderState();
@@ -474,16 +948,17 @@ export default function (pi: ExtensionAPI) {
474
948
  }
475
949
 
476
950
  ctx.ui.notify(
477
- `[AgentRouter Plugin]\n` +
478
- `- Active model: ${activeModel?.id || "none"} (${isAR ? "AgentRouter [yes]" : "Other Provider"})\n` +
479
- `- Package Priority: ${priorityStatus}\n` +
480
- `- API Key: ${maskedKey}\n` +
481
- `- Caching: Enabled (Prompt Cache + Session Affinity + Adaptive Thinking)\n` +
482
- `- Pacing Interval: ${minIntervalMs} ms (Measured from turn completion)\n` +
483
- `- Commands:\n` +
484
- ` /agentrouter key <key> (update API key)\n` +
485
- ` /agentrouter pacing <ms> (set request delay after completion)\n` +
486
- ` /agentrouter fix-order (move plugin above pi-cache-optimizer in settings.json)`,
951
+ `[AgentRouter Plugin v2.0.0]\n` +
952
+ `- Active model: ${activeModel?.id || "none"} (${isAR ? "AgentRouter [yes]" : "Other Provider"})\n` +
953
+ `- Package Priority: ${priorityStatus}\n` +
954
+ `- API Key: ${maskedKey}\n` +
955
+ `- Caching & Pacing: Enabled (${minIntervalMs} ms delay)\n` +
956
+ `- Commands:\n` +
957
+ ` /agentrouter check (probe live batch quotas & spending)\n` +
958
+ ` /agentrouter pricing (fetch live pricing table $/1M)\n` +
959
+ ` /agentrouter key <key> (update API key)\n` +
960
+ ` /agentrouter pacing <ms> (adjust rate limit delay)\n` +
961
+ ` /agentrouter fix-order (move plugin above pi-cache-optimizer)`,
487
962
  "info"
488
963
  );
489
964
  },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@madgagarin/pi-agentrouter",
3
- "version": "1.3.0",
4
- "description": "Pi coding agent extension for AgentRouter (GPT-5.6 Sol & Claude Opus 5) with rate-limit pacing and caching.",
3
+ "version": "2.0.0",
4
+ "description": "Official Pi Coding Agent extension for AgentRouter (agentrouter.org). Connects GPT-5.6 Sol, Claude Opus 5, DeepSeek V4 Flash, and GLM 5.3 with live USD pricing, batch quota health probe, prompt caching, and WAF protection.",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -11,17 +11,25 @@
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",
17
18
  "gpt-5.6-sol",
18
19
  "claude-opus-5",
19
20
  "claude-opus-4-8",
21
+ "deepseek-v4-flash",
22
+ "deepseek-v4f",
23
+ "glm-5.3",
24
+ "deepseek",
20
25
  "prompt-caching",
21
26
  "openai",
22
27
  "claude",
28
+ "anthropic",
23
29
  "llm-gateway",
24
- "ai-coding"
30
+ "llm-router",
31
+ "ai-coding",
32
+ "coding-agent"
25
33
  ],
26
34
  "author": "madgagarin (https://github.com/madgagarin)",
27
35
  "license": "MIT",