pi-hypercharm-provider 1.3.7 → 1.3.9
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/.pi/fabric/mesh/state.json +112 -2
- package/README.md +16 -11
- package/index.ts +21 -7
- package/models.json +81 -18
- package/oauth.ts +192 -0
- package/package.json +1 -1
- package/pnpm-workspace.yaml +0 -15
|
@@ -1,6 +1,112 @@
|
|
|
1
1
|
{
|
|
2
2
|
"format": 1,
|
|
3
|
-
"entries": {
|
|
3
|
+
"entries": {
|
|
4
|
+
"topology/peer-seq": {
|
|
5
|
+
"key": "topology/peer-seq",
|
|
6
|
+
"value": {
|
|
7
|
+
"format": 1,
|
|
8
|
+
"next": 1
|
|
9
|
+
},
|
|
10
|
+
"version": 1,
|
|
11
|
+
"updatedAt": 1788269765843,
|
|
12
|
+
"updatedBy": {
|
|
13
|
+
"id": "session:01a05d2e-8a1c-7bba-96fe-6e9eb4d7c42e",
|
|
14
|
+
"name": "main",
|
|
15
|
+
"kind": "main",
|
|
16
|
+
"sessionId": "01a05d2e-8a1c-7bba-96fe-6e9eb4d7c42e"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"topology/hosts/276282166ed2e87cc2e85abfc95904809b77ae8c5dd6d44b4ab841d33b333b8a": {
|
|
20
|
+
"key": "topology/hosts/276282166ed2e87cc2e85abfc95904809b77ae8c5dd6d44b4ab841d33b333b8a",
|
|
21
|
+
"value": {
|
|
22
|
+
"format": 1,
|
|
23
|
+
"id": "session:01a05d2e-8a1c-7bba-96fe-6e9eb4d7c42e",
|
|
24
|
+
"rootId": "session:01a05d2e-8a1c-7bba-96fe-6e9eb4d7c42e",
|
|
25
|
+
"identity": {
|
|
26
|
+
"id": "session:01a05d2e-8a1c-7bba-96fe-6e9eb4d7c42e",
|
|
27
|
+
"name": "main",
|
|
28
|
+
"kind": "main",
|
|
29
|
+
"sessionId": "01a05d2e-8a1c-7bba-96fe-6e9eb4d7c42e"
|
|
30
|
+
},
|
|
31
|
+
"startedAt": 1788269765777,
|
|
32
|
+
"updatedAt": 1788270826370,
|
|
33
|
+
"expiresAt": 1788270841370
|
|
34
|
+
},
|
|
35
|
+
"version": 324,
|
|
36
|
+
"updatedAt": 1788270826371,
|
|
37
|
+
"updatedBy": {
|
|
38
|
+
"id": "session:01a05d2e-8a1c-7bba-96fe-6e9eb4d7c42e",
|
|
39
|
+
"name": "main",
|
|
40
|
+
"kind": "main",
|
|
41
|
+
"sessionId": "01a05d2e-8a1c-7bba-96fe-6e9eb4d7c42e"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"sessions/01a05d2e-8a1c-7bba-96fe-6e9eb4d7c42e": {
|
|
45
|
+
"key": "sessions/01a05d2e-8a1c-7bba-96fe-6e9eb4d7c42e",
|
|
46
|
+
"value": {
|
|
47
|
+
"id": "session:01a05d2e-8a1c-7bba-96fe-6e9eb4d7c42e",
|
|
48
|
+
"name": "PHP-1",
|
|
49
|
+
"label": "PHP-1",
|
|
50
|
+
"kind": "peer",
|
|
51
|
+
"status": "running",
|
|
52
|
+
"runner": "pi",
|
|
53
|
+
"transport": "host",
|
|
54
|
+
"cwd": "/Users/monotykamary/VCS/working-remote/open-source/pi-hypercharm-provider",
|
|
55
|
+
"sessionId": "01a05d2e-8a1c-7bba-96fe-6e9eb4d7c42e",
|
|
56
|
+
"model": "hypercharm/glm-5.3-flash",
|
|
57
|
+
"thinking": "high",
|
|
58
|
+
"startedAt": 1788269765777,
|
|
59
|
+
"updatedAt": 1788270826370,
|
|
60
|
+
"pendingMessages": false,
|
|
61
|
+
"local": false
|
|
62
|
+
},
|
|
63
|
+
"version": 324,
|
|
64
|
+
"updatedAt": 1788270826372,
|
|
65
|
+
"updatedBy": {
|
|
66
|
+
"id": "session:01a05d2e-8a1c-7bba-96fe-6e9eb4d7c42e",
|
|
67
|
+
"name": "main",
|
|
68
|
+
"kind": "main",
|
|
69
|
+
"sessionId": "01a05d2e-8a1c-7bba-96fe-6e9eb4d7c42e"
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"topology/participants/276282166ed2e87cc2e85abfc95904809b77ae8c5dd6d44b4ab841d33b333b8a": {
|
|
73
|
+
"key": "topology/participants/276282166ed2e87cc2e85abfc95904809b77ae8c5dd6d44b4ab841d33b333b8a",
|
|
74
|
+
"value": {
|
|
75
|
+
"format": 1,
|
|
76
|
+
"id": "session:01a05d2e-8a1c-7bba-96fe-6e9eb4d7c42e",
|
|
77
|
+
"kind": "root",
|
|
78
|
+
"rootId": "session:01a05d2e-8a1c-7bba-96fe-6e9eb4d7c42e",
|
|
79
|
+
"ownerHostId": "session:01a05d2e-8a1c-7bba-96fe-6e9eb4d7c42e",
|
|
80
|
+
"ownerIdentityId": "session:01a05d2e-8a1c-7bba-96fe-6e9eb4d7c42e",
|
|
81
|
+
"name": "main",
|
|
82
|
+
"status": "running",
|
|
83
|
+
"runner": "pi",
|
|
84
|
+
"transport": "host",
|
|
85
|
+
"capabilities": [
|
|
86
|
+
"steer",
|
|
87
|
+
"followUp",
|
|
88
|
+
"fabric"
|
|
89
|
+
],
|
|
90
|
+
"cwd": "/Users/monotykamary/VCS/working-remote/open-source/pi-hypercharm-provider",
|
|
91
|
+
"sessionId": "01a05d2e-8a1c-7bba-96fe-6e9eb4d7c42e",
|
|
92
|
+
"model": "hypercharm/glm-5.3-flash",
|
|
93
|
+
"thinking": "high",
|
|
94
|
+
"startedAt": 1788269765777,
|
|
95
|
+
"updatedAt": 1788270826370,
|
|
96
|
+
"pendingMessages": false,
|
|
97
|
+
"controlProtocol": "v1",
|
|
98
|
+
"label": "PHP-1"
|
|
99
|
+
},
|
|
100
|
+
"version": 324,
|
|
101
|
+
"updatedAt": 1788270826373,
|
|
102
|
+
"updatedBy": {
|
|
103
|
+
"id": "session:01a05d2e-8a1c-7bba-96fe-6e9eb4d7c42e",
|
|
104
|
+
"name": "main",
|
|
105
|
+
"kind": "main",
|
|
106
|
+
"sessionId": "01a05d2e-8a1c-7bba-96fe-6e9eb4d7c42e"
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
},
|
|
4
110
|
"versions": {
|
|
5
111
|
"topology/hosts/e9758aa8c95305a8d60a8aa2c20fd53195591d479b2460e1273457793d3f1fec": 18,
|
|
6
112
|
"sessions/019fe2c8-1dd2-7435-9fa1-fa664831df18": 17,
|
|
@@ -13,7 +119,11 @@
|
|
|
13
119
|
"topology/participants/3195ffccdcff6cb767344b2d1af6c5f3e1e3e4a9906a81129ffaf24352769d2a": 217,
|
|
14
120
|
"topology/hosts/b98efd3cf26df4c69c6deae014475c4c0c080b1d0e5ef1cb0ebcc8dca7501c85": 388,
|
|
15
121
|
"sessions/01a00efe-4e23-7b2b-8583-2e8493780899": 387,
|
|
16
|
-
"topology/participants/b98efd3cf26df4c69c6deae014475c4c0c080b1d0e5ef1cb0ebcc8dca7501c85": 388
|
|
122
|
+
"topology/participants/b98efd3cf26df4c69c6deae014475c4c0c080b1d0e5ef1cb0ebcc8dca7501c85": 388,
|
|
123
|
+
"topology/peer-seq": 1,
|
|
124
|
+
"topology/hosts/276282166ed2e87cc2e85abfc95904809b77ae8c5dd6d44b4ab841d33b333b8a": 324,
|
|
125
|
+
"sessions/01a05d2e-8a1c-7bba-96fe-6e9eb4d7c42e": 324,
|
|
126
|
+
"topology/participants/276282166ed2e87cc2e85abfc95904809b77ae8c5dd6d44b4ab841d33b333b8a": 324
|
|
17
127
|
},
|
|
18
128
|
"tombstoneOrder": [
|
|
19
129
|
"sessions/019fe2c8-1dd2-7435-9fa1-fa664831df18",
|
package/README.md
CHANGED
|
@@ -15,9 +15,10 @@ _Hyperoptimized coding models — DeepSeek, GLM, Kimi, Qwen, MiniMax, Gemma, GPT
|
|
|
15
15
|
|
|
16
16
|
## Features
|
|
17
17
|
|
|
18
|
-
- **
|
|
18
|
+
- **31+ AI Models** including DeepSeek V4 Flash/Pro, GLM 5/5.1, Kimi K2.5/K2.6, Qwen3.6/3.7, MiniMax M2.7, Gemma 4, GPT-OSS, and Llama
|
|
19
19
|
- **DeepSeek Native Thinking** — Uses the `deepseek` thinking format for Charm Hyper requests, with native `reasoning_effort` on models that publish levels
|
|
20
20
|
- **OpenAI-compatible API** via Charm Hyper's `/v1/chat/completions` endpoint
|
|
21
|
+
- **OAuth Device Flow** — sign in with `/login` under the `hypercharm` provider (independent of the official provider's `hyper` registration)
|
|
21
22
|
- **Official Catalog Sync** from Charm's typed `/v1/provider` endpoint, matching `@charmland/pi-hyper-provider`
|
|
22
23
|
- **Reasoning Models** with provider-published on/off states and exact effort levels
|
|
23
24
|
- **Attachment Support** for models the official catalog marks as attachment-capable
|
|
@@ -30,18 +31,20 @@ _Hyperoptimized coding models — DeepSeek, GLM, Kimi, Qwen, MiniMax, Gemma, GPT
|
|
|
30
31
|
| DeepSeek V4 Flash 0731 | Text | 1.0M | 384K | $0.44 | $1.32 |
|
|
31
32
|
| DeepSeek V4 Pro | Text | 1.0M | 384K | $2.40 | $4.80 |
|
|
32
33
|
| DeepSeek V4 Pro 0813 | Text | 1.0M | 262K | $1.44 | $4.31 |
|
|
33
|
-
| Gemma 4 26B A4B | Text | 256K | 26K | $0.
|
|
34
|
-
| GLM
|
|
35
|
-
| GLM
|
|
34
|
+
| Gemma 4 26B A4B | Text | 256K | 26K | $0.12 | $0.42 |
|
|
35
|
+
| GLM 5.3 | Text | 1.0M | 262K | $1.52 | $4.79 |
|
|
36
|
+
| GLM 5.3 Flash | Text + Image | 1.0M | 131K | $0.16 | $0.54 |
|
|
37
|
+
| GLM-5 | Text | 203K | 20K | $0.91 | $2.93 |
|
|
38
|
+
| GLM-5.1 | Text | 203K | 3K | $1.33 | $4.31 |
|
|
36
39
|
| GLM-5.2 | Text | 1.0M | 33K | $1.52 | $4.79 |
|
|
37
|
-
| gpt-oss-120b | Text | 128K | 13K | $0.
|
|
40
|
+
| gpt-oss-120b | Text | 128K | 13K | $0.19 | $0.70 |
|
|
38
41
|
| Kimi K2.5 | Text | 262K | 26K | $0.54 | $2.85 |
|
|
39
42
|
| Kimi K2.6 | Text + Image | 262K | 26K | $1.03 | $4.36 |
|
|
40
43
|
| Kimi K2.7 Code | Text + Image | 262K | 16K | $1.03 | $4.36 |
|
|
41
44
|
| Kimi K3 | Text + Image | 1.0M | 16K | $3.27 | $16.33 |
|
|
42
45
|
| Llama 3.3 70B Instruct | Text | 128K | 13K | $0.61 | $1.04 |
|
|
43
|
-
| Llama 4 Maverick 17B 128E Instruct FP8 | Text | 430K | 43K | $0.
|
|
44
|
-
| MiniMax M2.7 | Text | 262K | 7K | $0.
|
|
46
|
+
| Llama 4 Maverick 17B 128E Instruct FP8 | Text | 430K | 43K | $0.27 | $0.90 |
|
|
47
|
+
| MiniMax M2.7 | Text | 262K | 7K | $0.42 | $1.61 |
|
|
45
48
|
| MiniMax M3 | Text + Image | 512K | 512K | $0.33 | $1.31 |
|
|
46
49
|
| Qwen3 Coder 480B A35B Instruct INT4 Mixed AR | Text | 106K | 11K | $0.45 | $2.15 |
|
|
47
50
|
| Qwen3 Next 80B A3B Instruct | Text | 262K | 26K | $0.12 | $1.14 |
|
|
@@ -179,13 +182,14 @@ Non-interactive toggles:
|
|
|
179
182
|
|
|
180
183
|
The HyperCharm API key can be configured in multiple ways (resolved in this order):
|
|
181
184
|
|
|
182
|
-
1.
|
|
185
|
+
1. **OAuth** — Run `pi`, send `/login`, and pick **HyperCharm**. This uses Hyper's device flow (open the verification URL, enter the code) and stores a refreshable OAuth credential under the `hypercharm` provider — fully independent of the official `@charmland/pi-hyper-provider` (`hyper`) registration, so both extensions can be installed side by side.
|
|
186
|
+
2. **`auth.json`** (recommended for API keys) — Add to `~/.pi/agent/auth.json`:
|
|
183
187
|
```json
|
|
184
188
|
{ "hypercharm": { "type": "api_key", "key": "your-api-key" } }
|
|
185
189
|
```
|
|
186
190
|
The `key` field supports literal values, env var names, and shell commands (prefix with `!`). See [pi's auth file docs](https://github.com/badlogic/pi-mono) for details.
|
|
187
|
-
|
|
188
|
-
|
|
191
|
+
3. **Runtime override** — Use the `--api-key` CLI flag
|
|
192
|
+
4. **Environment variable** — Set `HYPERCHARM_API_KEY`
|
|
189
193
|
|
|
190
194
|
Get your API key from [hyper.charm.land](https://hyper.charm.land).
|
|
191
195
|
|
|
@@ -215,7 +219,8 @@ Model metadata matches Charm's official [`@charmland/pi-hyper-provider`](https:/
|
|
|
215
219
|
- `thinkingFormat: "deepseek"`, which maps Pi thinking levels onto Hyper's `thinking` envelope
|
|
216
220
|
- `reasoning_effort` only when the catalog publishes concrete level names
|
|
217
221
|
- `maxTokensField: "max_tokens"` and `supportsStore: false`
|
|
218
|
-
-
|
|
222
|
+
- `cacheRead` from `cost_per_1m_out_cached` (discounted cached-output price) and `cacheWrite` from `cost_per_1m_in_cached`, exactly as the official provider maps them
|
|
223
|
+
- `/v1/credits` balance accepted in either hypercredits (`balance`) or USD (`balance_usd`, converted at the observed 20 hc = $1 rate)
|
|
219
224
|
|
|
220
225
|
`patch.json` is reserved only for a verified provider regression and is currently empty.
|
|
221
226
|
|
package/index.ts
CHANGED
|
@@ -73,11 +73,15 @@
|
|
|
73
73
|
* /hypercharm-status reset
|
|
74
74
|
*
|
|
75
75
|
* Usage:
|
|
76
|
-
* # Option 1:
|
|
76
|
+
* # Option 1: OAuth — run pi, send /login, and pick "HyperCharm"
|
|
77
|
+
* # (device flow; provider id "hypercharm", distinct from the official
|
|
78
|
+
* # @charmland/pi-hyper-provider registration "hyper")
|
|
79
|
+
*
|
|
80
|
+
* # Option 2: Store in auth.json
|
|
77
81
|
* # Add to ~/.pi/agent/auth.json:
|
|
78
82
|
* # "hypercharm": { "type": "api_key", "key": "your-api-key" }
|
|
79
83
|
*
|
|
80
|
-
* # Option
|
|
84
|
+
* # Option 3: Set as environment variable
|
|
81
85
|
* export HYPERCHARM_API_KEY=your-api-key
|
|
82
86
|
*
|
|
83
87
|
* # Run pi with the extension
|
|
@@ -90,6 +94,7 @@
|
|
|
90
94
|
|
|
91
95
|
import { clampThinkingLevel, streamOpenAICompletions } from "@earendil-works/pi-ai/compat";
|
|
92
96
|
import type { AssistantMessageEventStream, SimpleStreamOptions } from "@earendil-works/pi-ai/compat";
|
|
97
|
+
import { USER_AGENT, loginHypercharm, refreshHypercharmToken } from "./oauth";
|
|
93
98
|
import { getAgentDir, type ExtensionAPI, type ExtensionContext, type ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
94
99
|
import modelsData from "./models.json" with { type: "json" };
|
|
95
100
|
import customModelsData from "./custom-models.json" with { type: "json" };
|
|
@@ -302,7 +307,7 @@ function transformApiModel(apiModel: any): JsonModel | null {
|
|
|
302
307
|
async function fetchLiveModels(apiKey: string, signal?: AbortSignal): Promise<JsonModel[] | null> {
|
|
303
308
|
try {
|
|
304
309
|
const response = await fetch(MODELS_URL, {
|
|
305
|
-
headers: { Authorization: `Bearer ${apiKey}
|
|
310
|
+
headers: { Authorization: `Bearer ${apiKey}`, "User-Agent": USER_AGENT },
|
|
306
311
|
signal: signal ? AbortSignal.any([AbortSignal.timeout(LIVE_FETCH_TIMEOUT_MS), signal]) : AbortSignal.timeout(LIVE_FETCH_TIMEOUT_MS),
|
|
307
312
|
});
|
|
308
313
|
if (!response.ok) return null;
|
|
@@ -634,7 +639,7 @@ let metaFetched = false;
|
|
|
634
639
|
async function fetchJsonGet(url: string, apiKey: string, signal?: AbortSignal): Promise<any | null> {
|
|
635
640
|
try {
|
|
636
641
|
const response = await fetch(url, {
|
|
637
|
-
headers: { Authorization: `Bearer ${apiKey}
|
|
642
|
+
headers: { Authorization: `Bearer ${apiKey}`, "User-Agent": USER_AGENT },
|
|
638
643
|
signal: signal
|
|
639
644
|
? AbortSignal.any([AbortSignal.timeout(ACCOUNT_FETCH_TIMEOUT_MS), signal])
|
|
640
645
|
: AbortSignal.timeout(ACCOUNT_FETCH_TIMEOUT_MS),
|
|
@@ -656,9 +661,12 @@ function refreshCredits(apiKey: string | undefined, signal: AbortSignal | undefi
|
|
|
656
661
|
try {
|
|
657
662
|
const data = await fetchJsonGet(`${BASE_URL}/credits`, apiKey, signal);
|
|
658
663
|
if (data === null) return;
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
664
|
+
// /credits can report hypercredits ("balance") or USD ("balance_usd",
|
|
665
|
+
// USD-billed accounts). Handle both at the observed 20 hc = $1 rate so a
|
|
666
|
+
// server-side unit switch can never silently freeze the balance readout.
|
|
667
|
+
const rawBalance = typeof data?.balance === "number" ? data.balance : typeof data?.balance_usd === "number" ? data.balance_usd * 20 : undefined;
|
|
668
|
+
if (typeof rawBalance === "number" && Number.isFinite(rawBalance)) {
|
|
669
|
+
account.balance = rawBalance;
|
|
662
670
|
}
|
|
663
671
|
} finally {
|
|
664
672
|
creditsInFlight = null;
|
|
@@ -1007,6 +1015,12 @@ function makeProviderConfig(models: JsonModel[] = currentModels) {
|
|
|
1007
1015
|
api: "hypercharm",
|
|
1008
1016
|
models,
|
|
1009
1017
|
streamSimple: streamHypercharm,
|
|
1018
|
+
oauth: {
|
|
1019
|
+
name: "HyperCharm",
|
|
1020
|
+
login: (callbacks) => loginHypercharm(callbacks),
|
|
1021
|
+
refreshToken: (credentials, signal) => refreshHypercharmToken(credentials, signal),
|
|
1022
|
+
getApiKey: (credentials) => String(credentials.access ?? ""),
|
|
1023
|
+
},
|
|
1010
1024
|
};
|
|
1011
1025
|
}
|
|
1012
1026
|
|
package/models.json
CHANGED
|
@@ -140,10 +140,10 @@
|
|
|
140
140
|
"text"
|
|
141
141
|
],
|
|
142
142
|
"cost": {
|
|
143
|
-
"input": 0.
|
|
144
|
-
"output": 0.
|
|
143
|
+
"input": 0.12,
|
|
144
|
+
"output": 0.42,
|
|
145
145
|
"cacheRead": 0,
|
|
146
|
-
"cacheWrite": 0.
|
|
146
|
+
"cacheWrite": 0.06
|
|
147
147
|
},
|
|
148
148
|
"contextWindow": 256000,
|
|
149
149
|
"maxTokens": 25600,
|
|
@@ -154,6 +154,69 @@
|
|
|
154
154
|
"maxTokensField": "max_tokens"
|
|
155
155
|
}
|
|
156
156
|
},
|
|
157
|
+
{
|
|
158
|
+
"id": "glm-5.3",
|
|
159
|
+
"name": "GLM 5.3",
|
|
160
|
+
"reasoning": true,
|
|
161
|
+
"thinkingLevelMap": {
|
|
162
|
+
"off": null,
|
|
163
|
+
"minimal": null,
|
|
164
|
+
"low": "low",
|
|
165
|
+
"medium": null,
|
|
166
|
+
"high": "high",
|
|
167
|
+
"xhigh": null,
|
|
168
|
+
"max": "max"
|
|
169
|
+
},
|
|
170
|
+
"input": [
|
|
171
|
+
"text"
|
|
172
|
+
],
|
|
173
|
+
"cost": {
|
|
174
|
+
"input": 1.52432,
|
|
175
|
+
"output": 4.79072,
|
|
176
|
+
"cacheRead": 0.283088,
|
|
177
|
+
"cacheWrite": 0
|
|
178
|
+
},
|
|
179
|
+
"contextWindow": 1048576,
|
|
180
|
+
"maxTokens": 262144,
|
|
181
|
+
"compat": {
|
|
182
|
+
"supportsStore": false,
|
|
183
|
+
"supportsReasoningEffort": true,
|
|
184
|
+
"thinkingFormat": "deepseek",
|
|
185
|
+
"maxTokensField": "max_tokens"
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
"id": "glm-5.3-flash",
|
|
190
|
+
"name": "GLM 5.3 Flash",
|
|
191
|
+
"reasoning": true,
|
|
192
|
+
"thinkingLevelMap": {
|
|
193
|
+
"off": null,
|
|
194
|
+
"minimal": null,
|
|
195
|
+
"low": "low",
|
|
196
|
+
"medium": null,
|
|
197
|
+
"high": "high",
|
|
198
|
+
"xhigh": null,
|
|
199
|
+
"max": "max"
|
|
200
|
+
},
|
|
201
|
+
"input": [
|
|
202
|
+
"text",
|
|
203
|
+
"image"
|
|
204
|
+
],
|
|
205
|
+
"cost": {
|
|
206
|
+
"input": 0.16332,
|
|
207
|
+
"output": 0.5444,
|
|
208
|
+
"cacheRead": 0.031575200000000005,
|
|
209
|
+
"cacheWrite": 0
|
|
210
|
+
},
|
|
211
|
+
"contextWindow": 1048576,
|
|
212
|
+
"maxTokens": 131072,
|
|
213
|
+
"compat": {
|
|
214
|
+
"supportsStore": false,
|
|
215
|
+
"supportsReasoningEffort": true,
|
|
216
|
+
"thinkingFormat": "deepseek",
|
|
217
|
+
"maxTokensField": "max_tokens"
|
|
218
|
+
}
|
|
219
|
+
},
|
|
157
220
|
{
|
|
158
221
|
"id": "glm-5",
|
|
159
222
|
"name": "GLM-5",
|
|
@@ -171,10 +234,10 @@
|
|
|
171
234
|
"text"
|
|
172
235
|
],
|
|
173
236
|
"cost": {
|
|
174
|
-
"input": 0.
|
|
175
|
-
"output": 2.
|
|
237
|
+
"input": 0.91,
|
|
238
|
+
"output": 2.934,
|
|
176
239
|
"cacheRead": 0,
|
|
177
|
-
"cacheWrite": 0.
|
|
240
|
+
"cacheWrite": 0.455
|
|
178
241
|
},
|
|
179
242
|
"contextWindow": 202752,
|
|
180
243
|
"maxTokens": 20275,
|
|
@@ -202,10 +265,10 @@
|
|
|
202
265
|
"text"
|
|
203
266
|
],
|
|
204
267
|
"cost": {
|
|
205
|
-
"input": 1.
|
|
206
|
-
"output": 4.
|
|
268
|
+
"input": 1.332,
|
|
269
|
+
"output": 4.312,
|
|
207
270
|
"cacheRead": 0,
|
|
208
|
-
"cacheWrite": 0.
|
|
271
|
+
"cacheWrite": 0.666
|
|
209
272
|
},
|
|
210
273
|
"contextWindow": 202750,
|
|
211
274
|
"maxTokens": 3276,
|
|
@@ -264,10 +327,10 @@
|
|
|
264
327
|
"text"
|
|
265
328
|
],
|
|
266
329
|
"cost": {
|
|
267
|
-
"input": 0.
|
|
268
|
-
"output": 0.
|
|
330
|
+
"input": 0.188,
|
|
331
|
+
"output": 0.7,
|
|
269
332
|
"cacheRead": 0,
|
|
270
|
-
"cacheWrite": 0.
|
|
333
|
+
"cacheWrite": 0.094
|
|
271
334
|
},
|
|
272
335
|
"contextWindow": 128072,
|
|
273
336
|
"maxTokens": 13107,
|
|
@@ -453,10 +516,10 @@
|
|
|
453
516
|
"text"
|
|
454
517
|
],
|
|
455
518
|
"cost": {
|
|
456
|
-
"input": 0.
|
|
457
|
-
"output": 0.
|
|
519
|
+
"input": 0.274,
|
|
520
|
+
"output": 0.8992,
|
|
458
521
|
"cacheRead": 0,
|
|
459
|
-
"cacheWrite": 0.
|
|
522
|
+
"cacheWrite": 0.137
|
|
460
523
|
},
|
|
461
524
|
"contextWindow": 430000,
|
|
462
525
|
"maxTokens": 43000,
|
|
@@ -475,10 +538,10 @@
|
|
|
475
538
|
"text"
|
|
476
539
|
],
|
|
477
540
|
"cost": {
|
|
478
|
-
"input": 0.
|
|
479
|
-
"output": 1.
|
|
541
|
+
"input": 0.424,
|
|
542
|
+
"output": 1.612,
|
|
480
543
|
"cacheRead": 0,
|
|
481
|
-
"cacheWrite": 0.
|
|
544
|
+
"cacheWrite": 0.212
|
|
482
545
|
},
|
|
483
546
|
"contextWindow": 262100,
|
|
484
547
|
"maxTokens": 6553,
|
package/oauth.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HyperCharm OAuth device flow (provider id "hypercharm").
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the device flow from @charmland/pi-hyper-provider against
|
|
5
|
+
* hyper.charm.land, but registers under our own provider id ("hypercharm",
|
|
6
|
+
* oauth display name "HyperCharm") so it never collides with the official
|
|
7
|
+
* provider's "hyper" registration when both extensions are installed.
|
|
8
|
+
*
|
|
9
|
+
* Note: both providers register the device under the same name
|
|
10
|
+
* (`Pi (<hostname>)`), so /v1/devices entries are indistinguishable per host
|
|
11
|
+
* when the official provider is also signed in — the status widget's OAuth
|
|
12
|
+
* days-left readout may match either session.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { hostname } from "os";
|
|
16
|
+
import type { OAuthCredentials, OAuthDeviceCodeInfo, OAuthLoginCallbacks } from "@earendil-works/pi-ai";
|
|
17
|
+
import pkg from "./package.json" with { type: "json" };
|
|
18
|
+
|
|
19
|
+
const BASE_URL = "https://hyper.charm.land";
|
|
20
|
+
const OAUTH_FETCH_TIMEOUT_MS = 30_000;
|
|
21
|
+
const DEFAULT_POLL_INTERVAL_SECONDS = 5;
|
|
22
|
+
const MIN_POLL_INTERVAL_MS = 1_000;
|
|
23
|
+
const SLOW_DOWN_INCREMENT_MS = 5_000;
|
|
24
|
+
const TOKEN_EXPIRY_BUFFER_MS = 30_000;
|
|
25
|
+
const CANCEL_MESSAGE = "Login cancelled";
|
|
26
|
+
const TIMEOUT_MESSAGE = "HyperCharm device flow timed out";
|
|
27
|
+
|
|
28
|
+
export const USER_AGENT = `pi-hypercharm-provider/${(pkg as { version?: string }).version ?? "0.0.0"}`;
|
|
29
|
+
|
|
30
|
+
function deviceName(): string {
|
|
31
|
+
const host = hostname();
|
|
32
|
+
return host ? `Pi (${host})` : "Pi";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function hyperHeaders(extra: Record<string, string> = {}): Record<string, string> {
|
|
36
|
+
return { "Content-Type": "application/json", "User-Agent": USER_AGENT, ...extra };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function oauthFetch(url: string, init: RequestInit, signal?: AbortSignal): Promise<Response> {
|
|
40
|
+
const timeout = AbortSignal.timeout(OAUTH_FETCH_TIMEOUT_MS);
|
|
41
|
+
return fetch(url, { ...init, signal: signal ? AbortSignal.any([signal, timeout]) : timeout });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
45
|
+
if (signal?.aborted) throw new Error(CANCEL_MESSAGE);
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const timer = setTimeout(resolve, ms);
|
|
48
|
+
signal?.addEventListener(
|
|
49
|
+
"abort",
|
|
50
|
+
() => {
|
|
51
|
+
clearTimeout(timer);
|
|
52
|
+
reject(new Error(CANCEL_MESSAGE));
|
|
53
|
+
},
|
|
54
|
+
{ once: true },
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface DeviceAuthResponse {
|
|
60
|
+
device_code: string;
|
|
61
|
+
expires_in: number;
|
|
62
|
+
user_code: string;
|
|
63
|
+
verification_url: string;
|
|
64
|
+
interval?: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type DevicePollResponse =
|
|
68
|
+
| { refresh_token: string; team_id?: string; team_name?: string; user_id?: string }
|
|
69
|
+
| { error: string; error_description?: string };
|
|
70
|
+
|
|
71
|
+
interface TokenExchangeResponse {
|
|
72
|
+
access_token: string;
|
|
73
|
+
refresh_token: string;
|
|
74
|
+
expires_in?: number;
|
|
75
|
+
expires_at?: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function initiateDeviceAuth(signal?: AbortSignal): Promise<DeviceAuthResponse> {
|
|
79
|
+
const res = await oauthFetch(`${BASE_URL}/device/auth`, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: hyperHeaders(),
|
|
82
|
+
body: JSON.stringify({ device_name: deviceName() }),
|
|
83
|
+
signal,
|
|
84
|
+
});
|
|
85
|
+
if (!res.ok) throw new Error(`Hyper device auth failed: HTTP ${res.status}`);
|
|
86
|
+
const payload = await res.json();
|
|
87
|
+
if (
|
|
88
|
+
typeof payload?.device_code !== "string" ||
|
|
89
|
+
typeof payload?.user_code !== "string" ||
|
|
90
|
+
typeof payload?.verification_url !== "string" ||
|
|
91
|
+
typeof payload?.expires_in !== "number"
|
|
92
|
+
) {
|
|
93
|
+
throw new Error("Hyper device auth response is invalid");
|
|
94
|
+
}
|
|
95
|
+
return payload;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function pollDeviceAuth(
|
|
99
|
+
auth: DeviceAuthResponse,
|
|
100
|
+
signal?: AbortSignal,
|
|
101
|
+
): Promise<{ refresh_token: string; team_name?: string }> {
|
|
102
|
+
const deadline = Date.now() + auth.expires_in * 1000;
|
|
103
|
+
let intervalMs = Math.max(MIN_POLL_INTERVAL_MS, (auth.interval ?? DEFAULT_POLL_INTERVAL_SECONDS) * 1000);
|
|
104
|
+
|
|
105
|
+
// Server-specified cadence starts after the first wait (device codes are
|
|
106
|
+
// never ready immediately; polling sooner only earns slow_down responses).
|
|
107
|
+
await abortableSleep(Math.min(intervalMs, Math.max(0, deadline - Date.now())), signal);
|
|
108
|
+
|
|
109
|
+
while (Date.now() < deadline) {
|
|
110
|
+
if (signal?.aborted) throw new Error(CANCEL_MESSAGE);
|
|
111
|
+
const res = await fetch(`${BASE_URL}/device/auth/${encodeURIComponent(auth.device_code)}`, {
|
|
112
|
+
headers: hyperHeaders(),
|
|
113
|
+
signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(OAUTH_FETCH_TIMEOUT_MS)]) : AbortSignal.timeout(OAUTH_FETCH_TIMEOUT_MS),
|
|
114
|
+
});
|
|
115
|
+
if (!res.ok) throw new Error(`Hyper device poll failed: HTTP ${res.status}`);
|
|
116
|
+
const payload = await res.json();
|
|
117
|
+
|
|
118
|
+
if (typeof payload?.refresh_token === "string") {
|
|
119
|
+
return { refresh_token: payload.refresh_token, team_name: payload.team_name };
|
|
120
|
+
}
|
|
121
|
+
if (payload?.error === "authorization_pending") {
|
|
122
|
+
// fall through to the interval sleep
|
|
123
|
+
} else if (payload?.error === "slow_down") {
|
|
124
|
+
intervalMs = Math.max(MIN_POLL_INTERVAL_MS, intervalMs + SLOW_DOWN_INCREMENT_MS);
|
|
125
|
+
} else {
|
|
126
|
+
throw new Error(`Hyper device authorization failed: ${payload?.error_description ?? payload?.error ?? "unknown error"}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const remainingMs = deadline - Date.now();
|
|
130
|
+
if (remainingMs <= 0) break;
|
|
131
|
+
await abortableSleep(Math.min(intervalMs, remainingMs), signal);
|
|
132
|
+
}
|
|
133
|
+
throw new Error(TIMEOUT_MESSAGE);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function exchangeRefreshToken(refreshToken: string, signal?: AbortSignal): Promise<TokenExchangeResponse> {
|
|
137
|
+
const res = await fetch(`${BASE_URL}/token/exchange`, {
|
|
138
|
+
method: "POST",
|
|
139
|
+
headers: hyperHeaders(),
|
|
140
|
+
body: JSON.stringify({ refresh_token: refreshToken }),
|
|
141
|
+
signal,
|
|
142
|
+
});
|
|
143
|
+
if (!res.ok) {
|
|
144
|
+
const err = new Error(`Hyper token exchange failed: HTTP ${res.status}`);
|
|
145
|
+
if (res.status === 401) {
|
|
146
|
+
throw new Error("Your Hyper session is no longer valid. Run /login and re-authenticate with HyperCharm.", { cause: err });
|
|
147
|
+
}
|
|
148
|
+
throw err;
|
|
149
|
+
}
|
|
150
|
+
const payload = (await res.json()) as TokenExchangeResponse;
|
|
151
|
+
if (typeof payload?.access_token !== "string") throw new Error("Hyper token exchange response is invalid");
|
|
152
|
+
return payload;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function tokenExpiresAtMs(token: TokenExchangeResponse): number {
|
|
156
|
+
const now = Date.now();
|
|
157
|
+
const expiresAt = typeof token.expires_in === "number" ? now + token.expires_in * 1000 : Number(token.expires_at) * 1000;
|
|
158
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= now) {
|
|
159
|
+
throw new Error("Hyper token exchange response contains an expired token expiry");
|
|
160
|
+
}
|
|
161
|
+
return expiresAt - Math.min(TOKEN_EXPIRY_BUFFER_MS, Math.floor((expiresAt - now) / 2));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function toCredentials(token: TokenExchangeResponse, fallbackRefresh: string, teamName?: string): OAuthCredentials {
|
|
165
|
+
return {
|
|
166
|
+
type: "oauth",
|
|
167
|
+
refresh: token.refresh_token || fallbackRefresh,
|
|
168
|
+
access: token.access_token,
|
|
169
|
+
expires: tokenExpiresAtMs(token),
|
|
170
|
+
...(teamName ? { teamName } : {}),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Device-flow login for the "hypercharm" provider (/login flow). */
|
|
175
|
+
export async function loginHypercharm(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
|
|
176
|
+
const deviceAuth = await initiateDeviceAuth(callbacks.signal);
|
|
177
|
+
callbacks.onDeviceCode({
|
|
178
|
+
userCode: deviceAuth.user_code,
|
|
179
|
+
verificationUri: deviceAuth.verification_url,
|
|
180
|
+
intervalSeconds: deviceAuth.interval,
|
|
181
|
+
expiresInSeconds: deviceAuth.expires_in,
|
|
182
|
+
});
|
|
183
|
+
const devicePoll = await pollDeviceAuth(deviceAuth, callbacks.signal);
|
|
184
|
+
const token = await exchangeRefreshToken(devicePoll.refresh_token, callbacks.signal);
|
|
185
|
+
return toCredentials(token, devicePoll.refresh_token, devicePoll.team_name);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Refresh an expired HyperCharm OAuth credential via the token exchange endpoint. */
|
|
189
|
+
export async function refreshHypercharmToken(credential: OAuthCredentials, signal?: AbortSignal): Promise<OAuthCredentials> {
|
|
190
|
+
const teamName = typeof credential.teamName === "string" && credential.teamName.trim() ? credential.teamName : undefined;
|
|
191
|
+
return toCredentials(await exchangeRefreshToken(credential.refresh, signal), credential.refresh, teamName);
|
|
192
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hypercharm-provider",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.9",
|
|
4
4
|
"description": "HyperCharm provider extension for pi - Access DeepSeek, GLM, Kimi, Qwen, MiniMax, Gemma, and GPT-OSS models through the Charm Hyper API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
package/pnpm-workspace.yaml
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
allowBuilds:
|
|
2
|
-
'@google/genai': set this to true or false
|
|
3
|
-
protobufjs: set this to true or false
|
|
4
|
-
onlyBuiltDependencies:
|
|
5
|
-
- '@google/genai'
|
|
6
|
-
- protobufjs
|
|
7
|
-
|
|
8
|
-
minimumReleaseAgeExclude:
|
|
9
|
-
- '@earendil-works/pi-agent-core@0.84.0 || 0.84.1 || 0.84.2 || 0.84.3 || 0.84.4'
|
|
10
|
-
- '@earendil-works/pi-ai@0.84.0 || 0.84.1 || 0.84.2 || 0.84.3 || 0.84.4'
|
|
11
|
-
- '@earendil-works/pi-client@0.84.0 || 0.84.1 || 0.84.2 || 0.84.3 || 0.84.4'
|
|
12
|
-
- '@earendil-works/pi-coding-agent@0.84.0 || 0.84.1 || 0.84.2 || 0.84.3 || 0.84.4'
|
|
13
|
-
- '@earendil-works/pi-protocol@0.84.0 || 0.84.1 || 0.84.2 || 0.84.3 || 0.84.4'
|
|
14
|
-
- '@earendil-works/pi-telemetry@0.84.0 || 0.84.1 || 0.84.2 || 0.84.3 || 0.84.4'
|
|
15
|
-
- '@earendil-works/pi-tui@0.84.0 || 0.84.1 || 0.84.2 || 0.84.3 || 0.84.4'
|