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