@madgagarin/pi-agentrouter 1.4.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 +79 -89
  2. package/index.ts +555 -112
  3. package/package.json +10 -4
package/README.md CHANGED
@@ -1,140 +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
- **`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-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)**
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
+ ```
26
+
27
+ ### 3. Activate in Pi chat
28
+
29
+ ```text
30
+ /agentrouter key sk-your-agentrouter-key
31
+ ```
24
32
 
25
- *(If you already have an account, you can obtain your API key directly from your [AgentRouter Dashboard](https://agentrouter.org/dashboard)).*
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
+ ## 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.
40
+
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.
43
49
 
44
50
  ---
45
51
 
46
- ## πŸš€ Installation
52
+ ## Models & Pricing
47
53
 
48
- ### Option 1: Install via npm (Recommended)
49
- ```bash
50
- pi install npm:@madgagarin/pi-agentrouter
51
- ```
54
+ Rates are pulled directly from the [agentrouter.org](https://agentrouter.org) gateway API ($2.00 / 1M tokens base unit):
52
55
 
53
- ### Option 2: Install directly from GitHub
54
- ```bash
55
- pi install git:github.com/madgagarin/pi-agentrouter
56
- ```
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 |
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. If you hit a 402, run `/agentrouter check` to verify, and switch to `deepseek-v4-flash` or `glm-5.3` for unlimited coding.*
62
65
 
63
66
  ---
64
67
 
65
- ## βš™οΈ Configuration
68
+ ## In-Chat Commands
66
69
 
67
- ### 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. |
68
79
 
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
- ```
80
+ ---
74
81
 
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
- ```
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 |
80
91
 
81
92
  ---
82
93
 
83
- ### 2. Configure Model Cycling (`settings.json`)
94
+ ## Recommended `settings.json`
84
95
 
85
- 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:
86
97
 
87
98
  ```json
88
99
  {
89
100
  "defaultProvider": "agentrouter-openai",
90
- "defaultModel": "gpt-5.6-sol",
91
- "defaultThinkingLevel": "medium",
101
+ "defaultModel": "deepseek-v4-flash",
102
+ "defaultThinkingLevel": "low",
92
103
  "enabledModels": [
104
+ "agentrouter-openai/deepseek-v4-flash",
105
+ "agentrouter-openai/glm-5.3",
93
106
  "agentrouter-openai/gpt-5.6-sol",
94
- "agentrouter-openai/deepseek-v4f",
95
- "agentrouter-clode/claude-opus-4-8",
96
- "agentrouter-clode/claude-opus-5"
107
+ "agentrouter-clode/claude-opus-5",
108
+ "agentrouter-clode/claude-opus-4-8"
97
109
  ]
98
110
  }
99
111
  ```
100
112
 
101
113
  ---
102
114
 
103
- ## ⌨️ Hotkeys & Shortcuts
104
-
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. |
112
-
113
- ---
114
-
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
115
+ ## Notes & FAQ
126
116
 
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.
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.
129
119
 
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.
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.
132
122
 
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.
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`.
135
125
 
136
126
  ---
137
127
 
138
- ## πŸ“„ License
128
+ ## License
139
129
 
140
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 ?? 3500;
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,32 +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
- {
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
- ],
555
+ models: openaiModels,
204
556
  });
205
557
 
206
558
  pi.registerProvider("agentrouter-clode", {
@@ -214,42 +566,14 @@ export default function (pi: ExtensionAPI) {
214
566
  sendSessionAffinityHeaders: true,
215
567
  supportsEagerToolInputStreaming: false,
216
568
  },
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
- ],
569
+ models: claudeModels,
249
570
  });
571
+
572
+ return newModels;
250
573
  }
251
574
 
252
- registerAgentRouterProviders(currentApiKey);
575
+ const cachedPricing = loadCachedPricing();
576
+ registerAgentRouterProviders(currentApiKey, cachedPricing);
253
577
 
254
578
  function updatePromptRewriteEnvForModel(model?: any): void {
255
579
  if (isAgentRouter(model?.provider, model?.baseUrl)) {
@@ -259,13 +583,11 @@ export default function (pi: ExtensionAPI) {
259
583
  }
260
584
  }
261
585
 
262
- // Inter-request pacing to respect AgentRouter WAF rate limits and enforce canonical root prompt
263
586
  pi.on("before_provider_request", async (event, ctx) => {
264
587
  const provider = ((event as any)?.model?.provider || ctx?.model?.provider || "").toLowerCase();
265
- 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 || "";
266
589
 
267
590
  if (isAgentRouter(provider, baseUrl)) {
268
- // 1. Cross-process request pacing
269
591
  const lastEnd = getLastRequestEndTime();
270
592
  const now = Date.now();
271
593
  const elapsed = now - lastEnd;
@@ -274,15 +596,20 @@ export default function (pi: ExtensionAPI) {
274
596
  await new Promise((resolve) => setTimeout(resolve, waitMs));
275
597
  }
276
598
 
277
- // 2. Enforce canonical root system prompt in payload (Anthropic & OpenAI formats)
278
599
  const payload = event.payload;
279
600
  if (payload) {
280
601
  if (payload.system !== undefined) {
281
602
  payload.system = enforceCanonicalRootPrompt(payload.system);
282
603
  }
283
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
+ }
284
610
  const firstMsg = payload.messages[0];
285
- if (firstMsg && firstMsg.role === "system") {
611
+ if (firstMsg && (firstMsg.role === "system" || firstMsg.role === "developer")) {
612
+ firstMsg.role = "system";
286
613
  if (typeof firstMsg.content === "string") {
287
614
  firstMsg.content = enforceCanonicalRootPrompt(firstMsg.content);
288
615
  } else if (Array.isArray(firstMsg.content)) {
@@ -317,22 +644,40 @@ export default function (pi: ExtensionAPI) {
317
644
  });
318
645
 
319
646
  pi.on("session_start", async (_event, ctx) => {
647
+ currentApiKey = getEffectiveApiKey();
320
648
  updatePromptRewriteEnvForModel(ctx.model);
321
649
  setLastRequestEndTime(Date.now());
322
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
+
323
665
  const order = getPackageOrderState();
324
666
  if (order.needsFix && ctx.hasUI && typeof (ctx.ui as any).confirm === "function") {
325
667
  try {
326
668
  const confirmed = await (ctx.ui as any).confirm(
327
669
  "Pi AgentRouter Package Priority",
328
670
  "@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?"
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?"
331
673
  );
332
674
  if (confirmed) {
333
675
  const success = fixPackagePriorityInSettings();
334
676
  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");
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
+ );
336
681
  }
337
682
  }
338
683
  } catch {}
@@ -438,7 +783,7 @@ export default function (pi: ExtensionAPI) {
438
783
  });
439
784
 
440
785
  pi.registerCommand("agentrouter", {
441
- description: "Manage AgentRouter plugin settings (status, API key, pacing interval)",
786
+ description: "Manage AgentRouter plugin settings, live quotas, and pricing (/agentrouter check, /agentrouter pricing)",
442
787
  handler: async (args, ctx) => {
443
788
  const parts = (args || "").trim().split(/\s+/);
444
789
  const action = parts[0]?.toLowerCase();
@@ -452,7 +797,7 @@ export default function (pi: ExtensionAPI) {
452
797
  }
453
798
  currentApiKey = cleanKey;
454
799
  saveConfig({ apiKey: cleanKey, minIntervalMs });
455
- registerAgentRouterProviders(cleanKey);
800
+ registerAgentRouterProviders(cleanKey, loadCachedPricing());
456
801
  ctx.ui.notify("AgentRouter API key updated successfully for all models.", "info");
457
802
  return;
458
803
  }
@@ -471,7 +816,10 @@ export default function (pi: ExtensionAPI) {
471
816
  }
472
817
  const success = fixPackagePriorityInSettings();
473
818
  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");
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
+ );
475
823
  } else {
476
824
  ctx.ui.notify("Failed to update settings.json.", "error");
477
825
  }
@@ -490,7 +838,101 @@ export default function (pi: ExtensionAPI) {
490
838
  return;
491
839
  }
492
840
 
493
- 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";
494
936
  const activeModel = ctx.model;
495
937
  const isAR = isAgentRouter(activeModel?.provider, (activeModel as any)?.baseUrl);
496
938
  const order = getPackageOrderState();
@@ -506,16 +948,17 @@ export default function (pi: ExtensionAPI) {
506
948
  }
507
949
 
508
950
  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)`,
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)`,
519
962
  "info"
520
963
  );
521
964
  },
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.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,19 +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",
20
22
  "deepseek-v4f",
23
+ "glm-5.3",
21
24
  "deepseek",
22
25
  "prompt-caching",
23
26
  "openai",
24
27
  "claude",
28
+ "anthropic",
25
29
  "llm-gateway",
26
- "ai-coding"
30
+ "llm-router",
31
+ "ai-coding",
32
+ "coding-agent"
27
33
  ],
28
34
  "author": "madgagarin (https://github.com/madgagarin)",
29
35
  "license": "MIT",