pi-provider-qoder-cn 0.2.3 → 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 +7 -12
  2. package/dist/index.js +76 -69
  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.
@@ -157,21 +157,16 @@ the currently saved endpoint and defaults to the personal account:
157
157
  Choose the Qoder CN endpoint (default: personal account)
158
158
  → Personal account — public gateway (qoder.com.cn)
159
159
  Enterprise account — use a VPC endpoint
160
- Enterprise account — <detected value> (detected: stored credential)
161
160
  ```
162
161
 
163
- - **Personal account** uses the public gateway and pins that choice, so a value
164
- left behind by another client cannot resurface. If `QODER_VPC_ENDPOINT` /
165
- `QODERCN_VPC_ENDPOINT` is set, it still wins — `/login` says so and warns
166
- that a personal pick reverts on restart until the variable is removed.
162
+ - **Personal account** uses the public gateway and pins that choice. If
163
+ `QODER_VPC_ENDPOINT` / `QODERCN_VPC_ENDPOINT` is set, it still wins —
164
+ `/login` says so and warns that a personal pick reverts on restart until the
165
+ variable is removed.
167
166
  - **Enterprise account** then prompts for the instance name or domain.
168
167
  - Switching back and forth is expected; re-running `/login` presents the saved
169
168
  endpoint so you can change it.
170
169
 
171
- A `vpc_endpoint` found in `~/.pi/agent/auth.json` is offered as the third option
172
- but never applied on its own. `/qoder-endpoint` with no argument also reports it
173
- under “Detected (not applied)”.
174
-
175
170
  ## Models
176
171
 
177
172
  | Model | id | Context | Thinking effort |
package/dist/index.js CHANGED
@@ -256,22 +256,6 @@ function readVpcEndpointFromSettings() {
256
256
  }
257
257
  return void 0;
258
258
  }
259
- function readVpcEndpointFromAuth() {
260
- try {
261
- const path = join2(piAgentDir(), "auth.json");
262
- if (!existsSync2(path)) return void 0;
263
- const auth = JSON.parse(readFileSync2(path, "utf8"));
264
- const entry = auth?.["qoder-cn"];
265
- const value = entry?.vpc_endpoint ?? entry?.vpcInstance;
266
- if (value !== void 0 && value !== "") return String(value);
267
- } catch {
268
- }
269
- return void 0;
270
- }
271
- function readInheritedVpcEndpoint() {
272
- const value = readVpcEndpointFromAuth();
273
- return value ? { value, source: "stored credential" } : void 0;
274
- }
275
259
  function readQoderVpcEndpoint() {
276
260
  const fromEnv = process.env.QODER_VPC_ENDPOINT || process.env.QODERCN_VPC_ENDPOINT;
277
261
  if (fromEnv) return fromEnv;
@@ -323,7 +307,20 @@ function saveQoderVpcEndpoint(raw) {
323
307
  try {
324
308
  const dir = piAgentDir();
325
309
  if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
326
- 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");
327
324
  } catch (error) {
328
325
  console.error("[pi-provider-qoder-cn] Failed to save qoder-cn-settings.json:", error);
329
326
  }
@@ -364,9 +361,6 @@ function getQoderBaseUrl(mode) {
364
361
  function getQoderOpenApiUrl(mode) {
365
362
  return getRegionValue(mode, "openApiUrl");
366
363
  }
367
- function getQoderCenterUrl(mode) {
368
- return getRegionValue(mode, "centerUrl");
369
- }
370
364
  function getQoderModelListURL(mode) {
371
365
  return `${getQoderBaseUrl(mode)}algo/api/v2/model/list?Encode=1`;
372
366
  }
@@ -383,7 +377,7 @@ function getQoderUsageURL(mode) {
383
377
  return `${getQoderOpenApiUrl(mode)}/api/v2/quota/usage`;
384
378
  }
385
379
  function getQoderRefreshURL(mode) {
386
- return `${getQoderCenterUrl(mode)}/algo/api/v3/user/refresh_token`;
380
+ return `${getQoderOpenApiUrl(mode)}/api/v1/jobToken/refresh`;
387
381
  }
388
382
  function getQoderDeviceLoginURL(codeChallenge, machineID, nonce) {
389
383
  const baseUrl = getQoderRegionConfig("global").deviceLoginUrl;
@@ -1146,7 +1140,7 @@ async function exchangeJobToken(pat, mode) {
1146
1140
  const parsed = Date.parse(data.expires_at);
1147
1141
  if (!Number.isNaN(parsed)) expiresAt = parsed;
1148
1142
  } else if (data.expires_in) {
1149
- expiresAt = Date.now() + data.expires_in;
1143
+ expiresAt = Date.now() + (data.expires_in > 86400 ? data.expires_in : data.expires_in * 1e3);
1150
1144
  }
1151
1145
  return {
1152
1146
  jobToken: data.token,
@@ -1200,7 +1194,7 @@ var init_pat = __esm({
1200
1194
  "use strict";
1201
1195
  init_cosy();
1202
1196
  init_region();
1203
- UA = "pi-provider-qoder";
1197
+ UA = "pi-provider-qoder-cn";
1204
1198
  PAT_REFRESH_PREFIX = "pat";
1205
1199
  }
1206
1200
  });
@@ -1245,21 +1239,13 @@ function parseExpiresAt(s, expiresInSeconds) {
1245
1239
  async function confirmCnEndpoint(callbacks, mode) {
1246
1240
  if (mode !== "cn") return;
1247
1241
  const current = getQoderCNEndpoints();
1248
- const inherited = readInheritedVpcEndpoint();
1249
1242
  const envNames = ["QODER_VPC_ENDPOINT", "QODERCN_VPC_ENDPOINT"].filter((name) => process.env[name]);
1250
1243
  const personalId = "personal";
1251
1244
  const enterpriseId = "enterprise";
1252
- const inheritedId = "enterprise-inherited";
1253
1245
  const options = [
1254
1246
  { id: personalId, label: "Personal account \u2014 public gateway (qoder.com.cn)" },
1255
1247
  { id: enterpriseId, label: "Enterprise account \u2014 use a VPC endpoint" }
1256
1248
  ];
1257
- if (inherited) {
1258
- options.push({
1259
- id: inheritedId,
1260
- label: `Enterprise account \u2014 ${inherited.value} (detected: ${inherited.source})`
1261
- });
1262
- }
1263
1249
  let message = current.isDefault ? "Choose the Qoder CN endpoint to log in against (default: personal account)" : `Choose the Qoder CN endpoint to log in against (current: ${current.raw})`;
1264
1250
  if (envNames.length > 0) {
1265
1251
  message += `
@@ -1275,10 +1261,6 @@ async function confirmCnEndpoint(callbacks, mode) {
1275
1261
  }
1276
1262
  return;
1277
1263
  }
1278
- if (choice === inheritedId && inherited) {
1279
- setQoderCNEndpoint(inherited.value);
1280
- return;
1281
- }
1282
1264
  if (choice === enterpriseId) {
1283
1265
  const prompt = getPrompt(callbacks);
1284
1266
  const raw = await prompt({
@@ -1361,7 +1343,15 @@ async function patLogin(callbacks, providedPat, mode) {
1361
1343
  throw new Error("No Personal Access Token provided");
1362
1344
  }
1363
1345
  getProgress(callbacks)?.("Exchanging access token...");
1364
- const creds = await credentialsFromPat(pat, mode);
1346
+ let creds;
1347
+ try {
1348
+ creds = await credentialsFromPat(pat, mode);
1349
+ } catch (error) {
1350
+ const detail = error instanceof Error ? error.message : String(error);
1351
+ throw new Error(
1352
+ `${detail}. Check that the PAT is copied in full (64 characters for pt-... tokens) and was issued for the selected endpoint.`
1353
+ );
1354
+ }
1365
1355
  getProgress(callbacks)?.("Login successful!");
1366
1356
  return creds;
1367
1357
  }
@@ -1400,7 +1390,7 @@ async function runDeviceFlow(callbacks) {
1400
1390
  method: "GET",
1401
1391
  headers: {
1402
1392
  Accept: "application/json",
1403
- "User-Agent": "pi-provider-qoder"
1393
+ "User-Agent": "pi-provider-qoder-cn"
1404
1394
  },
1405
1395
  signal: getSignal(callbacks)
1406
1396
  });
@@ -1425,7 +1415,7 @@ async function runDeviceFlow(callbacks) {
1425
1415
  headers: {
1426
1416
  Authorization: `Bearer ${tokenData.token}`,
1427
1417
  Accept: "application/json",
1428
- "User-Agent": "pi-provider-qoder"
1418
+ "User-Agent": "pi-provider-qoder-cn"
1429
1419
  }
1430
1420
  });
1431
1421
  if (userinfoRes.ok) {
@@ -1524,8 +1514,13 @@ function saveCredentialsToAuthFile(providerID, credentials) {
1524
1514
  if (!existsSync4(dir)) {
1525
1515
  mkdirSync4(dir, { recursive: true, mode: 448 });
1526
1516
  }
1527
- const existing = readAuthFileCached();
1528
- 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
+ }
1529
1524
  auth[providerID] = { type: "oauth", ...credentials };
1530
1525
  writeFileSync4(authPath, JSON.stringify(auth, null, 2), { encoding: "utf-8", mode: 384 });
1531
1526
  authFileMem = { path: authPath, data: auth };
@@ -1572,20 +1567,21 @@ async function resolveQoderIdentity(accessToken, providerID, mode) {
1572
1567
  const mem = identityCache.get(cacheKey);
1573
1568
  if (mem?.userID) return mem;
1574
1569
  const cached = getCachedCredentials(accessToken, providerID);
1575
- if (cached?.userID) {
1570
+ if (cached?.userID && cached.access === accessToken) {
1576
1571
  identityCache.set(cacheKey, cached);
1577
1572
  return cached;
1578
1573
  }
1579
1574
  const info = await fetchUserInfo(accessToken, mode);
1580
1575
  const machineID = getMachineId();
1576
+ const stored = readAuthFileCached()?.[providerID];
1581
1577
  const creds = {
1582
1578
  access: accessToken,
1583
1579
  userID: info.userID || "qoder-user",
1584
1580
  email: info.email || region.userEmailFallback,
1585
1581
  name: info.name || region.userNameFallback,
1586
1582
  machineID,
1587
- refresh: "",
1588
- expires: 0
1583
+ refresh: stored?.refresh || "",
1584
+ expires: stored?.expires || 0
1589
1585
  };
1590
1586
  identityCache.set(cacheKey, creds);
1591
1587
  saveCredentialsToAuthFile(providerID, creds);
@@ -1649,9 +1645,9 @@ async function refreshQoderTokenForMode(credentials, mode) {
1649
1645
  "Content-Type": "application/json",
1650
1646
  Authorization: `Bearer ${credentials.access}`,
1651
1647
  Accept: "application/json",
1652
- "User-Agent": "pi-provider-qoder"
1648
+ "User-Agent": "pi-provider-qoder-cn"
1653
1649
  },
1654
- body: JSON.stringify({ refreshToken })
1650
+ body: JSON.stringify({ refresh_token: refreshToken })
1655
1651
  });
1656
1652
  if (response.ok) {
1657
1653
  const data = await response.json();
@@ -1662,7 +1658,7 @@ async function refreshQoderTokenForMode(credentials, mode) {
1662
1658
  const parsed = Date.parse(data.expires_at);
1663
1659
  if (!Number.isNaN(parsed)) expireMs = parsed;
1664
1660
  } else if (data.expires_in) {
1665
- expireMs = Date.now() + data.expires_in * 1e3;
1661
+ expireMs = Date.now() + (data.expires_in > 86400 ? data.expires_in : data.expires_in * 1e3);
1666
1662
  }
1667
1663
  const refreshed = {
1668
1664
  ...credentials,
@@ -1713,7 +1709,7 @@ async function fetchQoderUsageForMode(credentials, mode) {
1713
1709
  headers: {
1714
1710
  Authorization: `Bearer ${credentials.access}`,
1715
1711
  Accept: "application/json",
1716
- "User-Agent": "pi-provider-qoder"
1712
+ "User-Agent": "pi-provider-qoder-cn"
1717
1713
  }
1718
1714
  });
1719
1715
  if (!response.ok) {
@@ -1721,27 +1717,30 @@ async function fetchQoderUsageForMode(credentials, mode) {
1721
1717
  }
1722
1718
  const raw = await response.json();
1723
1719
  const usageBuckets = [];
1724
- 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) {
1725
1723
  usageBuckets.push({
1726
1724
  id: "user-quota",
1727
1725
  label: "User Quota",
1728
- usedDisplay: raw.userQuota.used.toFixed(2),
1729
- limitDisplay: raw.userQuota.total.toFixed(2),
1730
- unit: raw.userQuota.unit,
1726
+ usedDisplay: userQuota.used.toFixed(2),
1727
+ limitDisplay: (userQuota.total ?? userQuota.cap ?? 0).toFixed(2),
1728
+ unit: userQuota.unit,
1731
1729
  resetAt: raw.expiresAt ? new Date(raw.expiresAt).toISOString() : void 0
1732
1730
  });
1733
1731
  }
1734
- if (raw.orgResourcePackage && raw.orgResourcePackage.total > 0) {
1732
+ const orgLimit = org?.cap ?? org?.total ?? 0;
1733
+ if (org && orgLimit > 0) {
1735
1734
  usageBuckets.push({
1736
1735
  id: "org-resource-package",
1737
1736
  label: "Org Resource Package",
1738
- usedDisplay: raw.orgResourcePackage.used.toFixed(2),
1739
- limitDisplay: raw.orgResourcePackage.total.toFixed(2),
1740
- unit: raw.orgResourcePackage.unit,
1737
+ usedDisplay: org.used.toFixed(2),
1738
+ limitDisplay: orgLimit.toFixed(2),
1739
+ unit: org.unit,
1741
1740
  resetAt: raw.expiresAt ? new Date(raw.expiresAt).toISOString() : void 0
1742
1741
  });
1743
1742
  }
1744
- 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` : "";
1745
1744
  return {
1746
1745
  summary: remainingText,
1747
1746
  subscriptionTitle: region.usageTitle,
@@ -1770,13 +1769,10 @@ function describeEndpoint() {
1770
1769
  async function runEndpointCommand(args, ctx) {
1771
1770
  const input = (args || "").trim();
1772
1771
  if (!input) {
1773
- const inherited = readInheritedVpcEndpoint();
1774
- const inheritedHint = inherited ? `
1775
- Detected (not applied): ${inherited.value} from ${inherited.source} \u2014 run /qoder-endpoint ${inherited.value} to use it.` : "";
1776
1772
  ctx?.ui?.notify(
1777
1773
  `Current Qoder CN endpoint: ${describeEndpoint()}
1778
1774
  To set: /qoder-endpoint <domain>
1779
- To reset: /qoder-endpoint default${inheritedHint}`,
1775
+ To reset: /qoder-endpoint default`,
1780
1776
  "info"
1781
1777
  );
1782
1778
  return;
@@ -1860,7 +1856,7 @@ function formatResetTime(expiresAt, now = Date.now()) {
1860
1856
  const hours = Math.floor(totalMinutes % 1440 / 60);
1861
1857
  const minutes = totalMinutes % 60;
1862
1858
  const relative = days > 0 ? `${days}d ${hours}h` : hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
1863
- return `${new Date(timestamp).toISOString().slice(0, 10)} (in ${relative})`;
1859
+ return `${new Date(timestamp).toLocaleDateString("sv")} (in ${relative})`;
1864
1860
  }
1865
1861
  function isBucketRow(row) {
1866
1862
  return typeof row.used === "number" || typeof row.total === "number";
@@ -1915,22 +1911,32 @@ function hasBucket(bucket) {
1915
1911
  if (!bucket) return false;
1916
1912
  return (bucket.total ?? 0) > 0 || (bucket.used ?? 0) > 0;
1917
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
+ }
1918
1919
  function formatQoderUsage(raw, mode, now = Date.now(), options = {}) {
1919
1920
  const color = options.color ?? false;
1920
1921
  const region = getQoderRegionConfig(mode);
1921
1922
  const danger = Boolean(raw?.isQuotaExceeded);
1922
1923
  const userType = raw?.userType ? ` (${raw.userType})` : "";
1923
- 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);
1924
1930
  const rows = [{ label: "Overall", percent: overallPercent, tail: "used", danger }];
1925
- const showUserQuota = hasBucket(raw?.userQuota);
1926
- const showAddOnQuota = hasBucket(raw?.addOnQuota);
1927
- if (showUserQuota) rows.push(bucketRow("Plan quota", raw.userQuota, danger));
1928
- 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));
1929
1935
  const notes = [];
1930
1936
  if (danger) notes.push({ label: "Status", value: "quota exceeded", color: ANSI.red });
1931
- if (!showUserQuota && !showAddOnQuota) notes.push({ label: "Buckets", value: "none returned" });
1937
+ if (!showUserQuota && !showAddOnQuota && !showOrg) notes.push({ label: "Buckets", value: "none returned" });
1932
1938
  if (raw?.isPlanQuotaProrated) notes.push({ label: "Note", value: "plan quota is prorated" });
1933
- notes.push({ label: "Resets", value: formatResetTime(raw?.expiresAt, now) });
1939
+ notes.push({ label: "Expires", value: formatResetTime(raw?.expiresAt, now) });
1934
1940
  const manageUrl = raw?.addOnQuota?.detailUrl || raw?.upgradeUrl || region.manageUrl;
1935
1941
  if (manageUrl) notes.push({ label: "Manage", value: manageUrl });
1936
1942
  const title = `${region.usageTitle}${userType}`;
@@ -2081,8 +2087,9 @@ function positiveNumber(value, fallback) {
2081
2087
  function parseQueueNotice(envelope) {
2082
2088
  const payload = digForQueue(envelope, 0);
2083
2089
  if (!payload) return null;
2090
+ const retryMs = positiveNumber(payload.retry_after_ms, 0) || positiveNumber(payload.retryAfterSeconds, DEFAULT_RETRY_SECONDS) * 1e3;
2084
2091
  return {
2085
- retryAfterMs: Math.min(positiveNumber(payload.retryAfterSeconds, DEFAULT_RETRY_SECONDS) * 1e3, MAX_WAIT_MS),
2092
+ retryAfterMs: Math.min(retryMs, MAX_WAIT_MS),
2086
2093
  waitTimeSeconds: positiveNumber(payload.waitTime, 0),
2087
2094
  queueCount: positiveNumber(payload.queueCount, 0)
2088
2095
  };
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "pi-provider-qoder-cn",
3
- "version": "0.2.3",
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",