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