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.
Files changed (3) hide show
  1. package/README.md +3 -3
  2. package/dist/index.js +66 -36
  3. 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
- Resets never
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
- - `Resets never` means the API reported the year-9999 sentinel, i.e. the
104
- allowance does not reset on a schedule.
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
- writeFileSync2(settingsFilePath(), JSON.stringify({ vpc_endpoint: raw || "" }, null, 2), "utf8");
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 `${getQoderCenterUrl(mode)}/algo/api/v3/user/refresh_token`;
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
- const existing = readAuthFileCached();
1508
- const auth = existing ? { ...existing } : {};
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
- if (raw.userQuota) {
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: raw.userQuota.used.toFixed(2),
1709
- limitDisplay: raw.userQuota.total.toFixed(2),
1710
- unit: raw.userQuota.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
- if (raw.orgResourcePackage && raw.orgResourcePackage.total > 0) {
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: raw.orgResourcePackage.used.toFixed(2),
1719
- limitDisplay: raw.orgResourcePackage.total.toFixed(2),
1720
- unit: raw.orgResourcePackage.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 = raw.userQuota ? `${raw.userQuota.remaining.toFixed(2)} ${raw.userQuota.unit} remaining` : "";
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).toISOString().slice(0, 10)} (in ${relative})`;
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 overallPercent = formatPercent(raw?.totalUsagePercentage);
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
- const showUserQuota = hasBucket(raw?.userQuota);
1903
- const showAddOnQuota = hasBucket(raw?.addOnQuota);
1904
- if (showUserQuota) rows.push(bucketRow("Plan quota", raw.userQuota, danger));
1905
- if (showAddOnQuota) rows.push(bucketRow("Add-on", raw.addOnQuota, danger));
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: "Resets", value: formatResetTime(raw?.expiresAt, now) });
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(positiveNumber(payload.retryAfterSeconds, DEFAULT_RETRY_SECONDS) * 1e3, MAX_WAIT_MS),
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.4",
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",