@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.
- package/README.md +79 -89
- package/index.ts +555 -112
- package/package.json +10 -4
package/README.md
CHANGED
|
@@ -1,140 +1,130 @@
|
|
|
1
|
-
# pi-agentrouter
|
|
1
|
+
# @madgagarin/pi-agentrouter
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@madgagarin/pi-agentrouter)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://pi.dev)
|
|
6
|
-
|
|
6
|
+
[](https://agentrouter.org)
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
##
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
### 1. Get your API key
|
|
17
18
|
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*(
|
|
33
|
+
*(Or set `export AGENTROUTER_API_KEY="sk-..."` in your shell).*
|
|
26
34
|
|
|
27
35
|
---
|
|
28
36
|
|
|
29
|
-
##
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
-
|
|
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
|
-
##
|
|
52
|
+
## Models & Pricing
|
|
47
53
|
|
|
48
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
68
|
+
## In-Chat Commands
|
|
66
69
|
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
94
|
+
## Recommended `settings.json`
|
|
84
95
|
|
|
85
|
-
|
|
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": "
|
|
91
|
-
"defaultThinkingLevel": "
|
|
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-
|
|
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
|
-
##
|
|
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
|
-
####
|
|
128
|
-
AgentRouter
|
|
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
|
-
####
|
|
131
|
-
No.
|
|
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
|
-
####
|
|
134
|
-
AgentRouter
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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():
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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 = (
|
|
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
|
-
|
|
330
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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": "
|
|
4
|
-
"description": "Pi
|
|
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
|
-
"
|
|
30
|
+
"llm-router",
|
|
31
|
+
"ai-coding",
|
|
32
|
+
"coding-agent"
|
|
27
33
|
],
|
|
28
34
|
"author": "madgagarin (https://github.com/madgagarin)",
|
|
29
35
|
"license": "MIT",
|