pi-hypercharm-provider 1.3.8 → 1.3.10
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 +26 -3
- package/README.md +10 -7
- package/index.ts +21 -7
- package/models.json +9 -9
- package/oauth.ts +192 -0
- package/package.json +1 -1
|
@@ -1,6 +1,22 @@
|
|
|
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
|
+
},
|
|
4
20
|
"versions": {
|
|
5
21
|
"topology/hosts/e9758aa8c95305a8d60a8aa2c20fd53195591d479b2460e1273457793d3f1fec": 18,
|
|
6
22
|
"sessions/019fe2c8-1dd2-7435-9fa1-fa664831df18": 17,
|
|
@@ -13,7 +29,11 @@
|
|
|
13
29
|
"topology/participants/3195ffccdcff6cb767344b2d1af6c5f3e1e3e4a9906a81129ffaf24352769d2a": 217,
|
|
14
30
|
"topology/hosts/b98efd3cf26df4c69c6deae014475c4c0c080b1d0e5ef1cb0ebcc8dca7501c85": 388,
|
|
15
31
|
"sessions/01a00efe-4e23-7b2b-8583-2e8493780899": 387,
|
|
16
|
-
"topology/participants/b98efd3cf26df4c69c6deae014475c4c0c080b1d0e5ef1cb0ebcc8dca7501c85": 388
|
|
32
|
+
"topology/participants/b98efd3cf26df4c69c6deae014475c4c0c080b1d0e5ef1cb0ebcc8dca7501c85": 388,
|
|
33
|
+
"topology/peer-seq": 1,
|
|
34
|
+
"topology/hosts/276282166ed2e87cc2e85abfc95904809b77ae8c5dd6d44b4ab841d33b333b8a": 385,
|
|
35
|
+
"sessions/01a05d2e-8a1c-7bba-96fe-6e9eb4d7c42e": 384,
|
|
36
|
+
"topology/participants/276282166ed2e87cc2e85abfc95904809b77ae8c5dd6d44b4ab841d33b333b8a": 385
|
|
17
37
|
},
|
|
18
38
|
"tombstoneOrder": [
|
|
19
39
|
"sessions/019fe2c8-1dd2-7435-9fa1-fa664831df18",
|
|
@@ -27,6 +47,9 @@
|
|
|
27
47
|
"topology/hosts/3195ffccdcff6cb767344b2d1af6c5f3e1e3e4a9906a81129ffaf24352769d2a",
|
|
28
48
|
"sessions/01a00efe-4e23-7b2b-8583-2e8493780899",
|
|
29
49
|
"topology/participants/b98efd3cf26df4c69c6deae014475c4c0c080b1d0e5ef1cb0ebcc8dca7501c85",
|
|
30
|
-
"topology/hosts/b98efd3cf26df4c69c6deae014475c4c0c080b1d0e5ef1cb0ebcc8dca7501c85"
|
|
50
|
+
"topology/hosts/b98efd3cf26df4c69c6deae014475c4c0c080b1d0e5ef1cb0ebcc8dca7501c85",
|
|
51
|
+
"sessions/01a05d2e-8a1c-7bba-96fe-6e9eb4d7c42e",
|
|
52
|
+
"topology/participants/276282166ed2e87cc2e85abfc95904809b77ae8c5dd6d44b4ab841d33b333b8a",
|
|
53
|
+
"topology/hosts/276282166ed2e87cc2e85abfc95904809b77ae8c5dd6d44b4ab841d33b333b8a"
|
|
31
54
|
]
|
|
32
55
|
}
|
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@ _Hyperoptimized coding models — DeepSeek, GLM, Kimi, Qwen, MiniMax, Gemma, GPT
|
|
|
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
|
|
@@ -36,14 +37,14 @@ _Hyperoptimized coding models — DeepSeek, GLM, Kimi, Qwen, MiniMax, Gemma, GPT
|
|
|
36
37
|
| GLM-5 | Text | 203K | 20K | $0.91 | $2.93 |
|
|
37
38
|
| GLM-5.1 | Text | 203K | 3K | $1.33 | $4.31 |
|
|
38
39
|
| GLM-5.2 | Text | 1.0M | 33K | $1.52 | $4.79 |
|
|
39
|
-
| gpt-oss-120b | Text | 128K | 13K | $0.19 | $0.
|
|
40
|
-
| Kimi K2.5 | Text | 262K | 26K | $0.
|
|
40
|
+
| gpt-oss-120b | Text | 128K | 13K | $0.19 | $0.63 |
|
|
41
|
+
| Kimi K2.5 | Text | 262K | 26K | $0.55 | $2.88 |
|
|
41
42
|
| Kimi K2.6 | Text + Image | 262K | 26K | $1.03 | $4.36 |
|
|
42
43
|
| Kimi K2.7 Code | Text + Image | 262K | 16K | $1.03 | $4.36 |
|
|
43
44
|
| Kimi K3 | Text + Image | 1.0M | 16K | $3.27 | $16.33 |
|
|
44
45
|
| Llama 3.3 70B Instruct | Text | 128K | 13K | $0.61 | $1.04 |
|
|
45
46
|
| Llama 4 Maverick 17B 128E Instruct FP8 | Text | 430K | 43K | $0.27 | $0.90 |
|
|
46
|
-
| MiniMax M2.7 | Text | 262K | 7K | $0.
|
|
47
|
+
| MiniMax M2.7 | Text | 262K | 7K | $0.40 | $1.50 |
|
|
47
48
|
| MiniMax M3 | Text + Image | 512K | 512K | $0.33 | $1.31 |
|
|
48
49
|
| Qwen3 Coder 480B A35B Instruct INT4 Mixed AR | Text | 106K | 11K | $0.45 | $2.15 |
|
|
49
50
|
| Qwen3 Next 80B A3B Instruct | Text | 262K | 26K | $0.12 | $1.14 |
|
|
@@ -181,13 +182,14 @@ Non-interactive toggles:
|
|
|
181
182
|
|
|
182
183
|
The HyperCharm API key can be configured in multiple ways (resolved in this order):
|
|
183
184
|
|
|
184
|
-
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`:
|
|
185
187
|
```json
|
|
186
188
|
{ "hypercharm": { "type": "api_key", "key": "your-api-key" } }
|
|
187
189
|
```
|
|
188
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.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
+
3. **Runtime override** — Use the `--api-key` CLI flag
|
|
192
|
+
4. **Environment variable** — Set `HYPERCHARM_API_KEY`
|
|
191
193
|
|
|
192
194
|
Get your API key from [hyper.charm.land](https://hyper.charm.land).
|
|
193
195
|
|
|
@@ -217,7 +219,8 @@ Model metadata matches Charm's official [`@charmland/pi-hyper-provider`](https:/
|
|
|
217
219
|
- `thinkingFormat: "deepseek"`, which maps Pi thinking levels onto Hyper's `thinking` envelope
|
|
218
220
|
- `reasoning_effort` only when the catalog publishes concrete level names
|
|
219
221
|
- `maxTokensField: "max_tokens"` and `supportsStore: false`
|
|
220
|
-
-
|
|
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)
|
|
221
224
|
|
|
222
225
|
`patch.json` is reserved only for a verified provider regression and is currently empty.
|
|
223
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
|
@@ -327,10 +327,10 @@
|
|
|
327
327
|
"text"
|
|
328
328
|
],
|
|
329
329
|
"cost": {
|
|
330
|
-
"input": 0.
|
|
331
|
-
"output": 0.
|
|
330
|
+
"input": 0.19,
|
|
331
|
+
"output": 0.63,
|
|
332
332
|
"cacheRead": 0,
|
|
333
|
-
"cacheWrite": 0.
|
|
333
|
+
"cacheWrite": 0.095
|
|
334
334
|
},
|
|
335
335
|
"contextWindow": 128072,
|
|
336
336
|
"maxTokens": 13107,
|
|
@@ -358,10 +358,10 @@
|
|
|
358
358
|
"text"
|
|
359
359
|
],
|
|
360
360
|
"cost": {
|
|
361
|
-
"input": 0.
|
|
362
|
-
"output": 2.
|
|
361
|
+
"input": 0.5504,
|
|
362
|
+
"output": 2.885,
|
|
363
363
|
"cacheRead": 0,
|
|
364
|
-
"cacheWrite": 0.
|
|
364
|
+
"cacheWrite": 0.2752
|
|
365
365
|
},
|
|
366
366
|
"contextWindow": 262144,
|
|
367
367
|
"maxTokens": 26214,
|
|
@@ -538,10 +538,10 @@
|
|
|
538
538
|
"text"
|
|
539
539
|
],
|
|
540
540
|
"cost": {
|
|
541
|
-
"input": 0.
|
|
542
|
-
"output": 1.
|
|
541
|
+
"input": 0.404,
|
|
542
|
+
"output": 1.496,
|
|
543
543
|
"cacheRead": 0,
|
|
544
|
-
"cacheWrite": 0.
|
|
544
|
+
"cacheWrite": 0.202
|
|
545
545
|
},
|
|
546
546
|
"contextWindow": 262100,
|
|
547
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.10",
|
|
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",
|