pi-provider-qoder-cn 0.2.4 → 0.2.5
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 +3 -3
- package/dist/index.js +66 -36
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -74,7 +74,7 @@ Qoder CN Plan (personal_standard)
|
|
|
74
74
|
Overall [███░░░░░░░░░░░░░░░░░░] 12.5% used
|
|
75
75
|
Plan quota [███░░░░░░░░░░░░░░░░░░] 250 / 2000 credits 12.5% 1750 left
|
|
76
76
|
Add-on [░░░░░░░░░░░░░░░░░░░░░] 0 / 700 credits 0% 700 left
|
|
77
|
-
|
|
77
|
+
Expires never
|
|
78
78
|
Manage https://qoder.com.cn/account/usage
|
|
79
79
|
```
|
|
80
80
|
|
|
@@ -100,8 +100,8 @@ Notes:
|
|
|
100
100
|
not reserve space for one it does not have.
|
|
101
101
|
- Colour is suppressed automatically for a `NO_COLOR` environment, a dumb
|
|
102
102
|
terminal, and any non-TTY output that is not the interactive UI.
|
|
103
|
-
- `
|
|
104
|
-
allowance
|
|
103
|
+
- `Expires never` means the API reported the year-9999 sentinel, i.e. the
|
|
104
|
+
allowance has no expiry date.
|
|
105
105
|
- `Note plan quota is prorated` appears after a mid-cycle plan change.
|
|
106
106
|
- The command uses the same stored token as chat requests, refreshing it first,
|
|
107
107
|
so it reports the same quota the model is actually billed against.
|
package/dist/index.js
CHANGED
|
@@ -307,7 +307,20 @@ function saveQoderVpcEndpoint(raw) {
|
|
|
307
307
|
try {
|
|
308
308
|
const dir = piAgentDir();
|
|
309
309
|
if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
|
|
310
|
-
|
|
310
|
+
const path = settingsFilePath();
|
|
311
|
+
let payload = {};
|
|
312
|
+
if (existsSync2(path)) {
|
|
313
|
+
try {
|
|
314
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
315
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
316
|
+
payload = parsed;
|
|
317
|
+
}
|
|
318
|
+
} catch {
|
|
319
|
+
payload = {};
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
payload.vpc_endpoint = raw || "";
|
|
323
|
+
writeFileSync2(path, JSON.stringify(payload, null, 2), "utf8");
|
|
311
324
|
} catch (error) {
|
|
312
325
|
console.error("[pi-provider-qoder-cn] Failed to save qoder-cn-settings.json:", error);
|
|
313
326
|
}
|
|
@@ -348,9 +361,6 @@ function getQoderBaseUrl(mode) {
|
|
|
348
361
|
function getQoderOpenApiUrl(mode) {
|
|
349
362
|
return getRegionValue(mode, "openApiUrl");
|
|
350
363
|
}
|
|
351
|
-
function getQoderCenterUrl(mode) {
|
|
352
|
-
return getRegionValue(mode, "centerUrl");
|
|
353
|
-
}
|
|
354
364
|
function getQoderModelListURL(mode) {
|
|
355
365
|
return `${getQoderBaseUrl(mode)}algo/api/v2/model/list?Encode=1`;
|
|
356
366
|
}
|
|
@@ -367,7 +377,7 @@ function getQoderUsageURL(mode) {
|
|
|
367
377
|
return `${getQoderOpenApiUrl(mode)}/api/v2/quota/usage`;
|
|
368
378
|
}
|
|
369
379
|
function getQoderRefreshURL(mode) {
|
|
370
|
-
return `${
|
|
380
|
+
return `${getQoderOpenApiUrl(mode)}/api/v1/jobToken/refresh`;
|
|
371
381
|
}
|
|
372
382
|
function getQoderDeviceLoginURL(codeChallenge, machineID, nonce) {
|
|
373
383
|
const baseUrl = getQoderRegionConfig("global").deviceLoginUrl;
|
|
@@ -1130,7 +1140,7 @@ async function exchangeJobToken(pat, mode) {
|
|
|
1130
1140
|
const parsed = Date.parse(data.expires_at);
|
|
1131
1141
|
if (!Number.isNaN(parsed)) expiresAt = parsed;
|
|
1132
1142
|
} else if (data.expires_in) {
|
|
1133
|
-
expiresAt = Date.now() + data.expires_in;
|
|
1143
|
+
expiresAt = Date.now() + (data.expires_in > 86400 ? data.expires_in : data.expires_in * 1e3);
|
|
1134
1144
|
}
|
|
1135
1145
|
return {
|
|
1136
1146
|
jobToken: data.token,
|
|
@@ -1184,7 +1194,7 @@ var init_pat = __esm({
|
|
|
1184
1194
|
"use strict";
|
|
1185
1195
|
init_cosy();
|
|
1186
1196
|
init_region();
|
|
1187
|
-
UA = "pi-provider-qoder";
|
|
1197
|
+
UA = "pi-provider-qoder-cn";
|
|
1188
1198
|
PAT_REFRESH_PREFIX = "pat";
|
|
1189
1199
|
}
|
|
1190
1200
|
});
|
|
@@ -1380,7 +1390,7 @@ async function runDeviceFlow(callbacks) {
|
|
|
1380
1390
|
method: "GET",
|
|
1381
1391
|
headers: {
|
|
1382
1392
|
Accept: "application/json",
|
|
1383
|
-
"User-Agent": "pi-provider-qoder"
|
|
1393
|
+
"User-Agent": "pi-provider-qoder-cn"
|
|
1384
1394
|
},
|
|
1385
1395
|
signal: getSignal(callbacks)
|
|
1386
1396
|
});
|
|
@@ -1405,7 +1415,7 @@ async function runDeviceFlow(callbacks) {
|
|
|
1405
1415
|
headers: {
|
|
1406
1416
|
Authorization: `Bearer ${tokenData.token}`,
|
|
1407
1417
|
Accept: "application/json",
|
|
1408
|
-
"User-Agent": "pi-provider-qoder"
|
|
1418
|
+
"User-Agent": "pi-provider-qoder-cn"
|
|
1409
1419
|
}
|
|
1410
1420
|
});
|
|
1411
1421
|
if (userinfoRes.ok) {
|
|
@@ -1504,8 +1514,13 @@ function saveCredentialsToAuthFile(providerID, credentials) {
|
|
|
1504
1514
|
if (!existsSync4(dir)) {
|
|
1505
1515
|
mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
1506
1516
|
}
|
|
1507
|
-
|
|
1508
|
-
|
|
1517
|
+
let auth;
|
|
1518
|
+
try {
|
|
1519
|
+
auth = JSON.parse(readFileSync4(authPath, "utf-8")) ?? {};
|
|
1520
|
+
} catch {
|
|
1521
|
+
const cached = readAuthFileCached();
|
|
1522
|
+
auth = cached ? { ...cached } : {};
|
|
1523
|
+
}
|
|
1509
1524
|
auth[providerID] = { type: "oauth", ...credentials };
|
|
1510
1525
|
writeFileSync4(authPath, JSON.stringify(auth, null, 2), { encoding: "utf-8", mode: 384 });
|
|
1511
1526
|
authFileMem = { path: authPath, data: auth };
|
|
@@ -1552,20 +1567,21 @@ async function resolveQoderIdentity(accessToken, providerID, mode) {
|
|
|
1552
1567
|
const mem = identityCache.get(cacheKey);
|
|
1553
1568
|
if (mem?.userID) return mem;
|
|
1554
1569
|
const cached = getCachedCredentials(accessToken, providerID);
|
|
1555
|
-
if (cached?.userID) {
|
|
1570
|
+
if (cached?.userID && cached.access === accessToken) {
|
|
1556
1571
|
identityCache.set(cacheKey, cached);
|
|
1557
1572
|
return cached;
|
|
1558
1573
|
}
|
|
1559
1574
|
const info = await fetchUserInfo(accessToken, mode);
|
|
1560
1575
|
const machineID = getMachineId();
|
|
1576
|
+
const stored = readAuthFileCached()?.[providerID];
|
|
1561
1577
|
const creds = {
|
|
1562
1578
|
access: accessToken,
|
|
1563
1579
|
userID: info.userID || "qoder-user",
|
|
1564
1580
|
email: info.email || region.userEmailFallback,
|
|
1565
1581
|
name: info.name || region.userNameFallback,
|
|
1566
1582
|
machineID,
|
|
1567
|
-
refresh: "",
|
|
1568
|
-
expires: 0
|
|
1583
|
+
refresh: stored?.refresh || "",
|
|
1584
|
+
expires: stored?.expires || 0
|
|
1569
1585
|
};
|
|
1570
1586
|
identityCache.set(cacheKey, creds);
|
|
1571
1587
|
saveCredentialsToAuthFile(providerID, creds);
|
|
@@ -1629,9 +1645,9 @@ async function refreshQoderTokenForMode(credentials, mode) {
|
|
|
1629
1645
|
"Content-Type": "application/json",
|
|
1630
1646
|
Authorization: `Bearer ${credentials.access}`,
|
|
1631
1647
|
Accept: "application/json",
|
|
1632
|
-
"User-Agent": "pi-provider-qoder"
|
|
1648
|
+
"User-Agent": "pi-provider-qoder-cn"
|
|
1633
1649
|
},
|
|
1634
|
-
body: JSON.stringify({ refreshToken })
|
|
1650
|
+
body: JSON.stringify({ refresh_token: refreshToken })
|
|
1635
1651
|
});
|
|
1636
1652
|
if (response.ok) {
|
|
1637
1653
|
const data = await response.json();
|
|
@@ -1642,7 +1658,7 @@ async function refreshQoderTokenForMode(credentials, mode) {
|
|
|
1642
1658
|
const parsed = Date.parse(data.expires_at);
|
|
1643
1659
|
if (!Number.isNaN(parsed)) expireMs = parsed;
|
|
1644
1660
|
} else if (data.expires_in) {
|
|
1645
|
-
expireMs = Date.now() + data.expires_in * 1e3;
|
|
1661
|
+
expireMs = Date.now() + (data.expires_in > 86400 ? data.expires_in : data.expires_in * 1e3);
|
|
1646
1662
|
}
|
|
1647
1663
|
const refreshed = {
|
|
1648
1664
|
...credentials,
|
|
@@ -1693,7 +1709,7 @@ async function fetchQoderUsageForMode(credentials, mode) {
|
|
|
1693
1709
|
headers: {
|
|
1694
1710
|
Authorization: `Bearer ${credentials.access}`,
|
|
1695
1711
|
Accept: "application/json",
|
|
1696
|
-
"User-Agent": "pi-provider-qoder"
|
|
1712
|
+
"User-Agent": "pi-provider-qoder-cn"
|
|
1697
1713
|
}
|
|
1698
1714
|
});
|
|
1699
1715
|
if (!response.ok) {
|
|
@@ -1701,27 +1717,30 @@ async function fetchQoderUsageForMode(credentials, mode) {
|
|
|
1701
1717
|
}
|
|
1702
1718
|
const raw = await response.json();
|
|
1703
1719
|
const usageBuckets = [];
|
|
1704
|
-
|
|
1720
|
+
const userQuota = raw.user_quota ?? raw.userQuota;
|
|
1721
|
+
const org = raw.org_resource_package ?? raw.orgResourcePackage ?? raw.shared_quota ?? raw.sharedQuota;
|
|
1722
|
+
if (userQuota) {
|
|
1705
1723
|
usageBuckets.push({
|
|
1706
1724
|
id: "user-quota",
|
|
1707
1725
|
label: "User Quota",
|
|
1708
|
-
usedDisplay:
|
|
1709
|
-
limitDisplay:
|
|
1710
|
-
unit:
|
|
1726
|
+
usedDisplay: userQuota.used.toFixed(2),
|
|
1727
|
+
limitDisplay: (userQuota.total ?? userQuota.cap ?? 0).toFixed(2),
|
|
1728
|
+
unit: userQuota.unit,
|
|
1711
1729
|
resetAt: raw.expiresAt ? new Date(raw.expiresAt).toISOString() : void 0
|
|
1712
1730
|
});
|
|
1713
1731
|
}
|
|
1714
|
-
|
|
1732
|
+
const orgLimit = org?.cap ?? org?.total ?? 0;
|
|
1733
|
+
if (org && orgLimit > 0) {
|
|
1715
1734
|
usageBuckets.push({
|
|
1716
1735
|
id: "org-resource-package",
|
|
1717
1736
|
label: "Org Resource Package",
|
|
1718
|
-
usedDisplay:
|
|
1719
|
-
limitDisplay:
|
|
1720
|
-
unit:
|
|
1737
|
+
usedDisplay: org.used.toFixed(2),
|
|
1738
|
+
limitDisplay: orgLimit.toFixed(2),
|
|
1739
|
+
unit: org.unit,
|
|
1721
1740
|
resetAt: raw.expiresAt ? new Date(raw.expiresAt).toISOString() : void 0
|
|
1722
1741
|
});
|
|
1723
1742
|
}
|
|
1724
|
-
const remainingText =
|
|
1743
|
+
const remainingText = userQuota ? `${userQuota.remaining.toFixed(2)} ${userQuota.unit} remaining` : org ? `${org.remaining.toFixed(2)} ${org.unit} remaining` : "";
|
|
1725
1744
|
return {
|
|
1726
1745
|
summary: remainingText,
|
|
1727
1746
|
subscriptionTitle: region.usageTitle,
|
|
@@ -1837,7 +1856,7 @@ function formatResetTime(expiresAt, now = Date.now()) {
|
|
|
1837
1856
|
const hours = Math.floor(totalMinutes % 1440 / 60);
|
|
1838
1857
|
const minutes = totalMinutes % 60;
|
|
1839
1858
|
const relative = days > 0 ? `${days}d ${hours}h` : hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
|
|
1840
|
-
return `${new Date(timestamp).
|
|
1859
|
+
return `${new Date(timestamp).toLocaleDateString("sv")} (in ${relative})`;
|
|
1841
1860
|
}
|
|
1842
1861
|
function isBucketRow(row) {
|
|
1843
1862
|
return typeof row.used === "number" || typeof row.total === "number";
|
|
@@ -1892,22 +1911,32 @@ function hasBucket(bucket) {
|
|
|
1892
1911
|
if (!bucket) return false;
|
|
1893
1912
|
return (bucket.total ?? 0) > 0 || (bucket.used ?? 0) > 0;
|
|
1894
1913
|
}
|
|
1914
|
+
function pickBucket(raw, camel) {
|
|
1915
|
+
if (!raw) return void 0;
|
|
1916
|
+
const snake = camel.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
1917
|
+
return raw[snake] ?? raw[camel];
|
|
1918
|
+
}
|
|
1895
1919
|
function formatQoderUsage(raw, mode, now = Date.now(), options = {}) {
|
|
1896
1920
|
const color = options.color ?? false;
|
|
1897
1921
|
const region = getQoderRegionConfig(mode);
|
|
1898
1922
|
const danger = Boolean(raw?.isQuotaExceeded);
|
|
1899
1923
|
const userType = raw?.userType ? ` (${raw.userType})` : "";
|
|
1900
|
-
const
|
|
1924
|
+
const showUserQuota = hasBucket(pickBucket(raw, "userQuota"));
|
|
1925
|
+
const showAddOnQuota = hasBucket(pickBucket(raw, "addOnQuota"));
|
|
1926
|
+
const org = pickBucket(raw, "orgResourcePackage") ?? pickBucket(raw, "sharedQuota");
|
|
1927
|
+
const showOrg = hasBucket(org);
|
|
1928
|
+
const orgPercent = typeof org?.used === "number" && typeof org?.cap === "number" && org.cap > 0 ? formatPercent(org.used / org.cap * 100) : void 0;
|
|
1929
|
+
const overallPercent = formatPercent(showOrg ? raw?.totalUsagePercentage || orgPercent : raw?.totalUsagePercentage);
|
|
1901
1930
|
const rows = [{ label: "Overall", percent: overallPercent, tail: "used", danger }];
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
if (
|
|
1905
|
-
|
|
1931
|
+
if (showUserQuota) rows.push(bucketRow("Plan quota", pickBucket(raw, "userQuota"), danger));
|
|
1932
|
+
if (showAddOnQuota) rows.push(bucketRow("Add-on", pickBucket(raw, "addOnQuota"), danger));
|
|
1933
|
+
if (showOrg && org)
|
|
1934
|
+
rows.push(bucketRow("Enterprise", { ...org, percentage: void 0, total: org.cap ?? org.total }, danger));
|
|
1906
1935
|
const notes = [];
|
|
1907
1936
|
if (danger) notes.push({ label: "Status", value: "quota exceeded", color: ANSI.red });
|
|
1908
|
-
if (!showUserQuota && !showAddOnQuota) notes.push({ label: "Buckets", value: "none returned" });
|
|
1937
|
+
if (!showUserQuota && !showAddOnQuota && !showOrg) notes.push({ label: "Buckets", value: "none returned" });
|
|
1909
1938
|
if (raw?.isPlanQuotaProrated) notes.push({ label: "Note", value: "plan quota is prorated" });
|
|
1910
|
-
notes.push({ label: "
|
|
1939
|
+
notes.push({ label: "Expires", value: formatResetTime(raw?.expiresAt, now) });
|
|
1911
1940
|
const manageUrl = raw?.addOnQuota?.detailUrl || raw?.upgradeUrl || region.manageUrl;
|
|
1912
1941
|
if (manageUrl) notes.push({ label: "Manage", value: manageUrl });
|
|
1913
1942
|
const title = `${region.usageTitle}${userType}`;
|
|
@@ -2058,8 +2087,9 @@ function positiveNumber(value, fallback) {
|
|
|
2058
2087
|
function parseQueueNotice(envelope) {
|
|
2059
2088
|
const payload = digForQueue(envelope, 0);
|
|
2060
2089
|
if (!payload) return null;
|
|
2090
|
+
const retryMs = positiveNumber(payload.retry_after_ms, 0) || positiveNumber(payload.retryAfterSeconds, DEFAULT_RETRY_SECONDS) * 1e3;
|
|
2061
2091
|
return {
|
|
2062
|
-
retryAfterMs: Math.min(
|
|
2092
|
+
retryAfterMs: Math.min(retryMs, MAX_WAIT_MS),
|
|
2063
2093
|
waitTimeSeconds: positiveNumber(payload.waitTime, 0),
|
|
2064
2094
|
queueCount: positiveNumber(payload.queueCount, 0)
|
|
2065
2095
|
};
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-provider-qoder-cn",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Pi extension for Qoder CN — PAT/OAuth auth, /qoder-cn.usage quota command, and enterprise VPC endpoint support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=18"
|
|
9
|
+
},
|
|
7
10
|
"author": "divingyu",
|
|
8
11
|
"repository": {
|
|
9
12
|
"type": "git",
|