pi-provider-qoder-cn 0.2.8 → 0.2.16

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/dist/index.js CHANGED
@@ -93,6 +93,14 @@ var init_cn_naming = __esm({
93
93
  });
94
94
 
95
95
  // src/cosy.ts
96
+ var cosy_exports = {};
97
+ __export(cosy_exports, {
98
+ QODER_CLIENT_TYPE: () => QODER_CLIENT_TYPE,
99
+ QODER_GATEWAY_COSY_VERSION: () => QODER_GATEWAY_COSY_VERSION,
100
+ QODER_OPENAPI_COSY_VERSION: () => QODER_OPENAPI_COSY_VERSION,
101
+ buildAuthHeaders: () => buildAuthHeaders,
102
+ getMachineId: () => getMachineId
103
+ });
96
104
  import crypto from "node:crypto";
97
105
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
98
106
  import { homedir } from "node:os";
@@ -379,6 +387,9 @@ function getQoderUsageURL(mode) {
379
387
  function getQoderRefreshURL(mode) {
380
388
  return `${getQoderOpenApiUrl(mode)}/api/v1/jobToken/refresh`;
381
389
  }
390
+ function getQoderDeviceRefreshURL(mode) {
391
+ return `${getQoderOpenApiUrl(mode)}/api/v1/deviceToken/refresh`;
392
+ }
382
393
  function getQoderDeviceLoginURL(codeChallenge, machineID, nonce) {
383
394
  const baseUrl = getQoderRegionConfig("global").deviceLoginUrl;
384
395
  if (!baseUrl) throw new Error("Qoder browser login URL is not configured");
@@ -428,7 +439,7 @@ var init_region = __esm({
428
439
  supportsBrowserLogin: false
429
440
  }
430
441
  };
431
- QODER_PROVIDER_MODES = ["cn"];
442
+ QODER_PROVIDER_MODES = ["cn", "global"];
432
443
  }
433
444
  });
434
445
 
@@ -1099,6 +1110,38 @@ var init_catalog = __esm({
1099
1110
  }
1100
1111
  });
1101
1112
 
1113
+ // src/auth/expiry.ts
1114
+ function resolveTokenExpiryMs(expiresAt, expiresIn, now = Date.now()) {
1115
+ if (expiresAt !== void 0 && expiresAt !== null && expiresAt !== "") {
1116
+ if (typeof expiresAt === "number" && Number.isFinite(expiresAt) && expiresAt > 0) {
1117
+ return expiresAt < 1e12 ? expiresAt * 1e3 : expiresAt;
1118
+ }
1119
+ const parsed = Date.parse(String(expiresAt));
1120
+ if (!Number.isNaN(parsed)) return parsed;
1121
+ const numeric = Number.parseInt(String(expiresAt), 10);
1122
+ if (!Number.isNaN(numeric) && numeric > 0) {
1123
+ return numeric < 1e12 ? numeric * 1e3 : numeric;
1124
+ }
1125
+ }
1126
+ if (typeof expiresIn === "number" && Number.isFinite(expiresIn) && expiresIn > 0) {
1127
+ return now + (expiresIn > ONE_DAY_SECONDS ? expiresIn : expiresIn * 1e3);
1128
+ }
1129
+ return now + FALLBACK_TTL_MS;
1130
+ }
1131
+ function toEpochMs(value, now = Date.now()) {
1132
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return void 0;
1133
+ const ms = value < 1e12 ? value * 1e3 : value;
1134
+ return ms > now + 365 * 864e5 ? now + FALLBACK_TTL_MS : ms;
1135
+ }
1136
+ var ONE_DAY_SECONDS, FALLBACK_TTL_MS;
1137
+ var init_expiry = __esm({
1138
+ "src/auth/expiry.ts"() {
1139
+ "use strict";
1140
+ ONE_DAY_SECONDS = 86400;
1141
+ FALLBACK_TTL_MS = 24 * 36e5;
1142
+ }
1143
+ });
1144
+
1102
1145
  // src/auth/pat.ts
1103
1146
  var pat_exports = {};
1104
1147
  __export(pat_exports, {
@@ -1145,13 +1188,7 @@ async function exchangeJobToken(pat, mode) {
1145
1188
  if (!data.token) {
1146
1189
  throw new Error("Qoder PAT exchange returned no job token");
1147
1190
  }
1148
- let expiresAt = Date.now() + 24 * 60 * 60 * 1e3;
1149
- if (data.expires_at) {
1150
- const parsed = Date.parse(data.expires_at);
1151
- if (!Number.isNaN(parsed)) expiresAt = parsed;
1152
- } else if (data.expires_in) {
1153
- expiresAt = Date.now() + (data.expires_in > 86400 ? data.expires_in : data.expires_in * 1e3);
1154
- }
1191
+ const expiresAt = resolveTokenExpiryMs(data.expires_at, data.expires_in);
1155
1192
  return {
1156
1193
  jobToken: data.token,
1157
1194
  jobRefreshToken: data.refresh_token || "",
@@ -1204,6 +1241,7 @@ var init_pat = __esm({
1204
1241
  "use strict";
1205
1242
  init_cosy();
1206
1243
  init_region();
1244
+ init_expiry();
1207
1245
  UA = "pi-provider-qoder-cn";
1208
1246
  PAT_REFRESH_PREFIX = "pat";
1209
1247
  }
@@ -1235,16 +1273,7 @@ function generatePKCE() {
1235
1273
  return { codeVerifier, codeChallenge };
1236
1274
  }
1237
1275
  function parseExpiresAt(s, expiresInSeconds) {
1238
- if (s) {
1239
- const t = Date.parse(s);
1240
- if (!Number.isNaN(t)) return t;
1241
- const ms = Number.parseInt(s, 10);
1242
- if (!Number.isNaN(ms) && ms > 0) return ms;
1243
- }
1244
- if (expiresInSeconds && expiresInSeconds > 0) {
1245
- return Date.now() + expiresInSeconds * 1e3;
1246
- }
1247
- return Date.now() + 30 * 24 * 60 * 60 * 1e3;
1276
+ return resolveTokenExpiryMs(s, expiresInSeconds);
1248
1277
  }
1249
1278
  async function confirmCnEndpoint(callbacks, mode) {
1250
1279
  if (mode !== "cn") return;
@@ -1415,6 +1444,9 @@ async function runDeviceFlow(callbacks) {
1415
1444
  if (!tokenData.token) {
1416
1445
  throw new Error("Device token poll returned empty access token");
1417
1446
  }
1447
+ if (!tokenData.refresh_token) {
1448
+ throw new Error("Device token poll returned no refresh token");
1449
+ }
1418
1450
  const expireMs = parseExpiresAt(tokenData.expires_at, tokenData.expires_in);
1419
1451
  getProgress(callbacks)?.("Fetching user profile...");
1420
1452
  let email = "";
@@ -1462,6 +1494,7 @@ var init_login = __esm({
1462
1494
  init_cosy();
1463
1495
  init_region();
1464
1496
  init_vpc();
1497
+ init_expiry();
1465
1498
  init_pat();
1466
1499
  }
1467
1500
  });
@@ -1475,12 +1508,12 @@ __export(oauth_exports, {
1475
1508
  getQoderPatForMode: () => getQoderPatForMode,
1476
1509
  loginQoderForMode: () => loginQoderForMode,
1477
1510
  refreshQoderTokenForMode: () => refreshQoderTokenForMode,
1478
- resolveQoderIdentity: () => resolveQoderIdentity
1511
+ resolveQoderIdentity: () => resolveQoderIdentity,
1512
+ saveCredentialsToAuthFile: () => saveCredentialsToAuthFile
1479
1513
  });
1480
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
1514
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync, rmSync, statSync, writeFileSync as writeFileSync4 } from "node:fs";
1481
1515
  import { homedir as homedir4 } from "node:os";
1482
1516
  import { dirname as dirname3, join as join4 } from "node:path";
1483
- import * as PiCodingAgent from "@earendil-works/pi-coding-agent";
1484
1517
  function getHomeDir4() {
1485
1518
  return process.env.HOME || process.env.USERPROFILE || homedir4();
1486
1519
  }
@@ -1493,17 +1526,17 @@ function clearQoderAuthMemCache() {
1493
1526
  }
1494
1527
  function readAuthFileCached() {
1495
1528
  const authPath = getAuthFilePath();
1496
- if (authFileMem !== void 0) {
1497
- if (authFileMem === null) return null;
1498
- if (authFileMem.path === authPath) return authFileMem.data;
1499
- }
1500
1529
  if (!existsSync4(authPath)) {
1501
1530
  authFileMem = null;
1502
1531
  return null;
1503
1532
  }
1504
1533
  try {
1534
+ const stat = statSync(authPath);
1535
+ if (authFileMem && authFileMem.path === authPath && authFileMem.mtimeMs === stat.mtimeMs) {
1536
+ return authFileMem.data;
1537
+ }
1505
1538
  const data = JSON.parse(readFileSync4(authPath, "utf-8"));
1506
- authFileMem = { path: authPath, data };
1539
+ authFileMem = { path: authPath, data, mtimeMs: stat.mtimeMs };
1507
1540
  return data;
1508
1541
  } catch {
1509
1542
  authFileMem = null;
@@ -1532,8 +1565,25 @@ function saveCredentialsToAuthFile(providerID, credentials) {
1532
1565
  auth = cached ? { ...cached } : {};
1533
1566
  }
1534
1567
  auth[providerID] = { type: "oauth", ...credentials };
1535
- writeFileSync4(authPath, JSON.stringify(auth, null, 2), { encoding: "utf-8", mode: 384 });
1536
- authFileMem = { path: authPath, data: auth };
1568
+ const tmp = `${authPath}.${process.pid}.${Date.now()}.tmp`;
1569
+ writeFileSync4(tmp, JSON.stringify(auth, null, 2), { encoding: "utf-8", mode: 384 });
1570
+ let published = false;
1571
+ for (let attempt = 0; attempt < 3 && !published; attempt++) {
1572
+ try {
1573
+ renameSync(tmp, authPath);
1574
+ published = true;
1575
+ } catch (err) {
1576
+ if (attempt === 2) {
1577
+ console.error(`[pi-provider-qoder] rename failed for ${authPath}, falling back to direct write:`, err);
1578
+ writeFileSync4(authPath, JSON.stringify(auth, null, 2), { encoding: "utf-8", mode: 384 });
1579
+ published = true;
1580
+ } else {
1581
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25 * (attempt + 1));
1582
+ }
1583
+ }
1584
+ }
1585
+ rmSync(tmp, { force: true });
1586
+ authFileMem = { path: authPath, data: auth, mtimeMs: statSync(authPath).mtimeMs };
1537
1587
  const q = credentials;
1538
1588
  if (q.access && q.userID) {
1539
1589
  identityCache.set(`${providerID}:${q.access}`, q);
@@ -1546,23 +1596,14 @@ async function autoLoginQoderFromEnvironment(providerID, mode) {
1546
1596
  const pat = getQoderPatForMode(mode);
1547
1597
  if (!pat) return;
1548
1598
  const credentials = await credentialsFromPat(pat, mode);
1549
- if (typeof AuthStorage2?.create === "function") {
1550
- try {
1551
- const authStorage = AuthStorage2.create();
1552
- authStorage.set(providerID, { type: "oauth", ...credentials });
1553
- } catch {
1554
- saveCredentialsToAuthFile(providerID, credentials);
1555
- }
1556
- } else {
1557
- saveCredentialsToAuthFile(providerID, credentials);
1558
- }
1599
+ saveCredentialsToAuthFile(providerID, credentials);
1559
1600
  const qCreds = credentials;
1560
1601
  await updateQoderModelsCache(qCreds.access, qCreds.userID, qCreds.name, qCreds.email, mode);
1561
1602
  }
1562
1603
  function getCachedCredentials(_accessToken, providerID = "qoder") {
1563
1604
  const auth = readAuthFileCached();
1564
1605
  if (!auth) return null;
1565
- const creds = auth[providerID] || (providerID === "qoder" ? auth.qoder : null);
1606
+ const creds = auth[providerID];
1566
1607
  if (creds?.userID || creds?.access) {
1567
1608
  if (creds.access && creds.userID) {
1568
1609
  identityCache.set(`${providerID}:${creds.access}`, creds);
@@ -1594,7 +1635,9 @@ async function resolveQoderIdentity(accessToken, providerID, mode) {
1594
1635
  expires: stored?.expires || 0
1595
1636
  };
1596
1637
  identityCache.set(cacheKey, creds);
1597
- saveCredentialsToAuthFile(providerID, creds);
1638
+ if (stored?.refresh && info.userID && (!stored.access || stored.access === accessToken)) {
1639
+ saveCredentialsToAuthFile(providerID, creds);
1640
+ }
1598
1641
  return creds;
1599
1642
  }
1600
1643
  async function loginQoderForMode(callbacks, mode) {
@@ -1614,62 +1657,93 @@ async function loginQoderForMode(callbacks, mode) {
1614
1657
  const creds = await interactiveLogin(callbacks, mode);
1615
1658
  try {
1616
1659
  const qCreds = creds;
1617
- updateQoderModelsCache(qCreds.access, qCreds.userID, qCreds.name, qCreds.email, mode).catch(() => {
1618
- });
1660
+ await updateQoderModelsCache(qCreds.access, qCreds.userID, qCreds.name, qCreds.email, mode);
1619
1661
  } catch {
1620
1662
  }
1621
1663
  saveCredentialsToAuthFile(providerID, creds);
1622
1664
  return creds;
1623
1665
  }
1624
- async function refreshQoderTokenForMode(credentials, mode) {
1666
+ async function refreshQoderTokenForMode(credentials, mode, signal) {
1667
+ const providerID = getQoderRegionConfig(mode).providerID;
1668
+ const existing = inFlightRefreshes.get(providerID);
1669
+ if (existing) return existing;
1670
+ const promise = (async () => {
1671
+ try {
1672
+ return await executeRefreshForMode(credentials, mode, signal);
1673
+ } finally {
1674
+ inFlightRefreshes.delete(providerID);
1675
+ }
1676
+ })();
1677
+ inFlightRefreshes.set(providerID, promise);
1678
+ return promise;
1679
+ }
1680
+ async function executeRefreshForMode(credentials, mode, signal) {
1681
+ const region = getQoderRegionConfig(mode);
1682
+ const providerID = region.providerID;
1625
1683
  if (isPatRefresh(credentials.refresh)) {
1626
1684
  const { pat } = decodePatRefresh(credentials.refresh);
1627
1685
  if (pat) {
1628
1686
  try {
1629
1687
  const refreshed = await credentialsFromPat(pat, mode);
1630
1688
  const qCreds = refreshed;
1689
+ saveCredentialsToAuthFile(providerID, refreshed);
1631
1690
  updateQoderModelsCache(qCreds.access, qCreds.userID, qCreds.name, qCreds.email, mode).catch(() => {
1632
1691
  });
1633
1692
  return refreshed;
1634
- } catch {
1693
+ } catch (error) {
1694
+ const detail = error instanceof Error ? error.message : String(error);
1695
+ throw new Error(
1696
+ `${region.loginName} credential refresh failed (PAT re-exchange): ${detail}. If this persists, the PAT may be revoked \u2014 run /login ${providerID}.`
1697
+ );
1635
1698
  }
1636
1699
  }
1637
- return {
1638
- ...credentials,
1639
- expires: Date.now() + 60 * 60 * 1e3
1640
- // extend 1 hour to retry later
1641
- };
1700
+ throw new Error(
1701
+ `${region.loginName} credential refresh failed: the stored refresh chain carries no PAT. Run /login ${providerID}.`
1702
+ );
1642
1703
  }
1643
1704
  const parts = credentials.refresh.split("|");
1644
1705
  const refreshToken = parts[0] || "";
1706
+ if (!refreshToken) {
1707
+ throw new Error(
1708
+ `${region.loginName} credential has no stored refresh chain (written by an older version). Run /login ${providerID} once.`
1709
+ );
1710
+ }
1645
1711
  const userID = parts[1] || "";
1646
1712
  const machineID = parts[2] || getMachineId();
1647
1713
  const prev = credentials;
1648
1714
  const prevName = prev.name || "";
1649
1715
  const prevEmail = prev.email || "";
1650
- const refreshURL = getQoderRefreshURL(mode);
1651
- try {
1652
- const response = await fetch(refreshURL, {
1653
- method: "POST",
1654
- headers: {
1655
- "Content-Type": "application/json",
1656
- Authorization: `Bearer ${credentials.access}`,
1657
- Accept: "application/json",
1658
- "User-Agent": "pi-provider-qoder-cn"
1659
- },
1660
- body: JSON.stringify({ refresh_token: refreshToken })
1661
- });
1716
+ const isDevice = refreshToken.startsWith("drt-");
1717
+ const refreshURL = isDevice ? getQoderDeviceRefreshURL(mode) : getQoderRefreshURL(mode);
1718
+ const body = JSON.stringify({ refresh_token: refreshToken });
1719
+ const failures = [];
1720
+ for (const withAuth of [true, false]) {
1721
+ let response;
1722
+ try {
1723
+ response = await fetch(refreshURL, {
1724
+ method: "POST",
1725
+ headers: {
1726
+ "Content-Type": "application/json",
1727
+ ...withAuth ? { Authorization: `Bearer ${credentials.access}` } : {},
1728
+ Accept: "application/json",
1729
+ "User-Agent": "pi-provider-qoder-cn"
1730
+ },
1731
+ body,
1732
+ signal
1733
+ });
1734
+ } catch (error) {
1735
+ if (signal?.aborted) throw error;
1736
+ failures.push(error instanceof Error ? error.message : String(error));
1737
+ break;
1738
+ }
1662
1739
  if (response.ok) {
1663
1740
  const data = await response.json();
1664
- const newAccess = data.token;
1665
- const newRefresh = data.refresh_token || refreshToken;
1666
- let expireMs = Date.now() + 30 * 24 * 60 * 60 * 1e3;
1667
- if (data.expires_at) {
1668
- const parsed = Date.parse(data.expires_at);
1669
- if (!Number.isNaN(parsed)) expireMs = parsed;
1670
- } else if (data.expires_in) {
1671
- expireMs = Date.now() + (data.expires_in > 86400 ? data.expires_in : data.expires_in * 1e3);
1741
+ const newAccess = typeof data.token === "string" && data.token ? data.token : data.access_token;
1742
+ if (typeof newAccess !== "string" || !newAccess) {
1743
+ throw new Error("token refresh response carried no access token");
1672
1744
  }
1745
+ const newRefresh = data.refresh_token || refreshToken;
1746
+ const expireMs = resolveTokenExpiryMs(data.expires_at, data.expires_in);
1673
1747
  const refreshed = {
1674
1748
  ...credentials,
1675
1749
  refresh: `${newRefresh}|${userID}|${machineID}`,
@@ -1680,30 +1754,29 @@ async function refreshQoderTokenForMode(credentials, mode) {
1680
1754
  name: prevName,
1681
1755
  machineID
1682
1756
  };
1757
+ saveCredentialsToAuthFile(providerID, refreshed);
1683
1758
  updateQoderModelsCache(newAccess, userID, prevName, prevEmail, mode).catch(() => {
1684
1759
  });
1685
1760
  return refreshed;
1686
1761
  }
1687
- } catch {
1762
+ const errText = await response.text();
1763
+ failures.push(`${response.status} ${response.statusText}: ${errText.slice(0, 120)}`);
1764
+ if (response.status !== 401 && response.status !== 403 || !withAuth) break;
1688
1765
  }
1689
- const refreshedFallback = {
1690
- ...credentials,
1691
- expires: Date.now() + 60 * 60 * 1e3
1692
- // extend for 1 hour
1693
- };
1694
- return refreshedFallback;
1766
+ throw new Error(`${region.loginName} token refresh failed (${failures.join(" | ")}). Run /login ${providerID}.`);
1695
1767
  }
1696
- var AuthStorage2, identityCache, authFileMem;
1768
+ var identityCache, authFileMem, inFlightRefreshes;
1697
1769
  var init_oauth = __esm({
1698
1770
  "src/auth/oauth.ts"() {
1699
1771
  "use strict";
1700
1772
  init_catalog();
1701
1773
  init_cosy();
1702
1774
  init_region();
1775
+ init_expiry();
1703
1776
  init_login();
1704
1777
  init_pat();
1705
- AuthStorage2 = PiCodingAgent.AuthStorage;
1706
1778
  identityCache = /* @__PURE__ */ new Map();
1779
+ inFlightRefreshes = /* @__PURE__ */ new Map();
1707
1780
  }
1708
1781
  });
1709
1782
 
@@ -1764,7 +1837,400 @@ async function fetchQoderUsageForMode(credentials, mode) {
1764
1837
  // src/index.ts
1765
1838
  init_catalog();
1766
1839
 
1840
+ // src/commands/claim.ts
1841
+ init_expiry();
1842
+ init_cosy();
1843
+ init_region();
1844
+ import { spawnSync } from "node:child_process";
1845
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
1846
+ import { homedir as homedir5, hostname } from "node:os";
1847
+ import { basename, dirname as dirname4, join as join5 } from "node:path";
1848
+ import { fileURLToPath } from "node:url";
1849
+ function piAgentDir2() {
1850
+ const home = process.env.HOME || process.env.USERPROFILE || homedir5();
1851
+ return join5(home, ".pi", "agent");
1852
+ }
1853
+ function getCheckinCachePath(mode = "cn") {
1854
+ const file = mode === "cn" ? "qoder-cn-checkin.json" : "qoder-checkin.json";
1855
+ return join5(piAgentDir2(), file);
1856
+ }
1857
+ function readCheckinCache(mode = "cn") {
1858
+ try {
1859
+ const file = getCheckinCachePath(mode);
1860
+ if (!existsSync5(file)) return null;
1861
+ return JSON.parse(readFileSync5(file, "utf8"));
1862
+ } catch {
1863
+ return null;
1864
+ }
1865
+ }
1866
+ function writeCheckinCache(info, mode = "cn") {
1867
+ try {
1868
+ const file = getCheckinCachePath(mode);
1869
+ const dir = dirname4(file);
1870
+ if (!existsSync5(dir)) mkdirSync5(dir, { recursive: true });
1871
+ writeFileSync5(file, JSON.stringify(info, null, 2), "utf8");
1872
+ return true;
1873
+ } catch {
1874
+ return false;
1875
+ }
1876
+ }
1877
+ var ANSI = {
1878
+ reset: "\x1B[0m",
1879
+ bold: "\x1B[1m",
1880
+ green: "\x1B[32m",
1881
+ yellow: "\x1B[33m",
1882
+ red: "\x1B[31m",
1883
+ cyan: "\x1B[36m"
1884
+ };
1885
+ function paint(text, code) {
1886
+ return code && text ? `${code}${text}${ANSI.reset}` : text;
1887
+ }
1888
+ function msUntilBeijing10AM(now = Date.now()) {
1889
+ const beijingOffset = 8 * 36e5;
1890
+ const beijingTime = now + beijingOffset;
1891
+ const beijingDate = new Date(beijingTime);
1892
+ let targetUtc = Date.UTC(beijingDate.getUTCFullYear(), beijingDate.getUTCMonth(), beijingDate.getUTCDate(), 10, 0, 0);
1893
+ if (beijingTime >= targetUtc) {
1894
+ targetUtc += 24 * 36e5;
1895
+ }
1896
+ return targetUtc - beijingTime;
1897
+ }
1898
+ function formatCountdownBeijing(ms) {
1899
+ const totalMinutes = Math.max(1, Math.round(ms / 6e4));
1900
+ const hours = Math.floor(totalMinutes / 60);
1901
+ const minutes = totalMinutes % 60;
1902
+ return hours > 0 ? `${hours}\u5C0F\u65F6${minutes}\u5206\u949F` : `${minutes}\u5206\u949F`;
1903
+ }
1904
+ function formatDateTime(isoOrMs) {
1905
+ if (!isoOrMs) return "n/a";
1906
+ const d = new Date(isoOrMs);
1907
+ if (Number.isNaN(d.getTime())) return String(isoOrMs);
1908
+ return new Intl.DateTimeFormat("zh-CN", {
1909
+ timeZone: "Asia/Shanghai",
1910
+ year: "numeric",
1911
+ month: "2-digit",
1912
+ day: "2-digit",
1913
+ hour: "2-digit",
1914
+ minute: "2-digit",
1915
+ second: "2-digit",
1916
+ hour12: false
1917
+ }).format(d);
1918
+ }
1919
+ var printableAscii = (v) => typeof v === "string" && v.trim().length > 0 && /^[\x20-\x7e]+$/.test(v.trim());
1920
+ var identityMemCache = /* @__PURE__ */ new Map();
1921
+ var IDENTITY_TTL_MS = 30 * 6e4;
1922
+ function candidateUmidExes() {
1923
+ const custom = process.env.QODER_UMID_EXE;
1924
+ if (custom) return [custom];
1925
+ const candidates = [];
1926
+ if (process.platform === "win32") {
1927
+ const moduleDir = dirname4(fileURLToPath(import.meta.url));
1928
+ const pkgRoot = basename(moduleDir) === "commands" ? dirname4(dirname4(moduleDir)) : dirname4(moduleDir);
1929
+ candidates.push(join5(pkgRoot, "bin", "runtime-info.exe"));
1930
+ }
1931
+ const localAppData = process.env.LOCALAPPDATA || join5(process.env.HOME || process.env.USERPROFILE || homedir5(), "AppData", "Local");
1932
+ for (const r of [join5(localAppData, "Programs", "Qoder"), "C:\\Program Files\\Qoder", "D:\\Qoder"]) {
1933
+ candidates.push(join5(r, "resources", "umid", "runtime-info.exe"));
1934
+ }
1935
+ return candidates;
1936
+ }
1937
+ function runUmidExe(exe, environment, accountId) {
1938
+ const res = spawnSync(exe, [String(environment), "--account-stdin"], {
1939
+ input: JSON.stringify({ account: accountId }),
1940
+ timeout: 1e4,
1941
+ windowsHide: true,
1942
+ encoding: "utf8",
1943
+ maxBuffer: 1024 * 1024
1944
+ });
1945
+ if (res.error) throw res.error;
1946
+ if (res.status !== 0) throw new Error(`runtime-info exited ${res.status}: ${String(res.stderr || "").slice(0, 120)}`);
1947
+ return String(res.stdout || "");
1948
+ }
1949
+ async function resolveSashMachineIdentity(mode, accountId) {
1950
+ if (!accountId) return null;
1951
+ const key = `${mode}:${accountId}`;
1952
+ const hit = identityMemCache.get(key);
1953
+ if (hit && hit.expiresAt > Date.now()) return hit.identity;
1954
+ const environment = mode === "global" ? 3 : 0;
1955
+ for (const exe of candidateUmidExes()) {
1956
+ if (!existsSync5(exe)) continue;
1957
+ try {
1958
+ const stdout = await runUmidExe(exe, environment, accountId);
1959
+ const parsed = JSON.parse(stdout);
1960
+ if (!printableAscii(parsed.machineToken) || !printableAscii(parsed.machineCode) || !printableAscii(parsed.machineType)) {
1961
+ continue;
1962
+ }
1963
+ const identity = {
1964
+ machineToken: parsed.machineToken.trim(),
1965
+ machineCode: parsed.machineCode.trim(),
1966
+ machineType: parsed.machineType.trim()
1967
+ };
1968
+ identityMemCache.set(key, { identity, expiresAt: Date.now() + IDENTITY_TTL_MS });
1969
+ return identity;
1970
+ } catch {
1971
+ }
1972
+ }
1973
+ return null;
1974
+ }
1975
+ function buildSashHeaders(accessToken, machineID, identity) {
1976
+ const headers = {
1977
+ Authorization: `Bearer ${accessToken}`,
1978
+ Accept: "application/json",
1979
+ "Content-Type": "application/json",
1980
+ "Cosy-ClientType": "10",
1981
+ "Cosy-Version": "1.13.3",
1982
+ "Cosy-MachineId": machineID,
1983
+ "Cosy-MachineOS": "x86_64_windows",
1984
+ "Cosy-MachineHostname": hostname(),
1985
+ "User-Agent": "Qoder"
1986
+ };
1987
+ if (identity) {
1988
+ headers["Cosy-MachineToken"] = identity.machineToken;
1989
+ headers["Cosy-MachineCode"] = identity.machineCode;
1990
+ headers["Cosy-MachineType"] = identity.machineType;
1991
+ }
1992
+ return headers;
1993
+ }
1994
+ async function fetchQoderCampaigns(accessToken, machineID, mode, identity) {
1995
+ const url = `${getQoderOpenApiUrl(mode)}/sash/api/v1/me/campaigns?forceRefresh=true`;
1996
+ const response = await fetch(url, {
1997
+ method: "GET",
1998
+ headers: buildSashHeaders(accessToken, machineID, identity),
1999
+ signal: AbortSignal.timeout(1e4)
2000
+ });
2001
+ const text = await response.text();
2002
+ let data;
2003
+ try {
2004
+ data = JSON.parse(text);
2005
+ } catch {
2006
+ data = void 0;
2007
+ }
2008
+ if (!response.ok) {
2009
+ const detail = data?.message || text.slice(0, 200);
2010
+ throw new Error(`Failed to query Qoder campaigns (${response.status}): ${detail}`);
2011
+ }
2012
+ if (!data) throw new Error("Qoder campaigns response was not JSON.");
2013
+ return data;
2014
+ }
2015
+ async function claimQoderCampaign(accessToken, machineID, campaignId, mode, identity) {
2016
+ const url = `${getQoderOpenApiUrl(mode)}/sash/api/v1/me/campaigns/${encodeURIComponent(campaignId)}/claim`;
2017
+ const response = await fetch(url, {
2018
+ method: "POST",
2019
+ headers: buildSashHeaders(accessToken, machineID, identity),
2020
+ body: JSON.stringify({}),
2021
+ signal: AbortSignal.timeout(1e4)
2022
+ });
2023
+ const text = await response.text();
2024
+ let data;
2025
+ try {
2026
+ data = JSON.parse(text);
2027
+ } catch {
2028
+ data = void 0;
2029
+ }
2030
+ if (!response.ok) {
2031
+ const detail = data?.message || text.slice(0, 200);
2032
+ throw new Error(`Failed to claim Qoder benefit (${response.status}): ${detail}`);
2033
+ }
2034
+ if (!data) throw new Error("Qoder claim response was not JSON.");
2035
+ return data;
2036
+ }
2037
+ async function fetchCheckinGrantInfo(accessToken, machineID, mode, identity) {
2038
+ const cached = readCheckinCache(mode);
2039
+ if (cached?.claimed && cached.expiresAt && cached.cacheUntil && Date.now() < cached.cacheUntil) {
2040
+ return cached;
2041
+ }
2042
+ try {
2043
+ const campaignsData = await fetchQoderCampaigns(accessToken, machineID, mode, identity);
2044
+ const campaigns = campaignsData.campaigns || [];
2045
+ const dailyCreditCampaign = campaigns.find(
2046
+ (c) => c.actionType === "CLAIM_BENEFIT" && (c.benefit?.kind === "CREDITS" || c.benefit?.amount === 100)
2047
+ ) || campaigns.find((c) => c.actionType === "CLAIM_BENEFIT");
2048
+ if (!dailyCreditCampaign) return null;
2049
+ const title = dailyCreditCampaign.placements?.[0]?.content?.zh?.title || "\u6BCF\u5929\u9886 100 Credits";
2050
+ const amount = dailyCreditCampaign.benefit?.amount || 100;
2051
+ const validityDays = dailyCreditCampaign.benefit?.validity?.days || 30;
2052
+ const endAtMs = toEpochMs(dailyCreditCampaign.endAt);
2053
+ const cacheUntil = endAtMs && endAtMs > Date.now() ? endAtMs : Date.now() + msUntilBeijing10AM();
2054
+ let info;
2055
+ if (dailyCreditCampaign.claimStatus === "CLAIMED") {
2056
+ try {
2057
+ const claimRes = await claimQoderCampaign(
2058
+ accessToken,
2059
+ machineID,
2060
+ dailyCreditCampaign.campaignId,
2061
+ mode,
2062
+ identity
2063
+ );
2064
+ info = {
2065
+ claimed: true,
2066
+ amount: claimRes.benefit?.amount || amount,
2067
+ claimedAt: claimRes.claimedAt || cached?.claimedAt,
2068
+ expiresAt: claimRes.expiresAt || cached?.expiresAt,
2069
+ validityDays: claimRes.benefit?.validity?.days || validityDays,
2070
+ campaignId: dailyCreditCampaign.campaignId,
2071
+ campaignTitle: title,
2072
+ cacheUntil
2073
+ };
2074
+ } catch {
2075
+ info = {
2076
+ claimed: true,
2077
+ amount,
2078
+ claimedAt: cached?.claimedAt,
2079
+ expiresAt: cached?.expiresAt,
2080
+ validityDays,
2081
+ campaignId: dailyCreditCampaign.campaignId,
2082
+ campaignTitle: title,
2083
+ cacheUntil
2084
+ };
2085
+ }
2086
+ } else {
2087
+ info = {
2088
+ claimed: false,
2089
+ amount,
2090
+ validityDays,
2091
+ campaignId: dailyCreditCampaign.campaignId,
2092
+ campaignTitle: title,
2093
+ cacheUntil
2094
+ };
2095
+ }
2096
+ writeCheckinCache(info, mode);
2097
+ return info;
2098
+ } catch {
2099
+ return readCheckinCache(mode);
2100
+ }
2101
+ }
2102
+ async function resolveCredentials(providerID, ctx) {
2103
+ let accessToken;
2104
+ try {
2105
+ accessToken = await ctx?.modelRegistry?.getApiKeyForProvider(providerID) || void 0;
2106
+ } catch {
2107
+ }
2108
+ const { getCachedCredentials: getCachedCredentials2 } = await Promise.resolve().then(() => (init_oauth(), oauth_exports));
2109
+ const stored = getCachedCredentials2("", providerID);
2110
+ accessToken = accessToken || stored?.access || void 0;
2111
+ if (!accessToken) return null;
2112
+ const machineID = stored?.machineID || getMachineId();
2113
+ const userID = stored?.userID || "";
2114
+ return { accessToken, machineID, userID };
2115
+ }
2116
+ async function runClaimCommand(mode, args, ctx) {
2117
+ const region = getQoderRegionConfig(mode);
2118
+ const providerID = region.providerID;
2119
+ const wantsRaw = ["json", "raw", "debug"].includes((args || "").trim().toLowerCase());
2120
+ try {
2121
+ const creds = await resolveCredentials(providerID, ctx);
2122
+ if (!creds) {
2123
+ const msg = mode === "cn" ? `\u672A\u627E\u5230 ${providerID} \u767B\u5F55\u51ED\u636E\uFF0C\u8BF7\u5148\u8FD0\u884C /login ${providerID}` : `No ${providerID} credentials found. Run /login ${providerID} first.`;
2124
+ ctx?.ui?.notify(msg, "warning");
2125
+ if (!ctx?.ui) console.warn(msg);
2126
+ return;
2127
+ }
2128
+ const { accessToken, machineID, userID } = creds;
2129
+ const identity = await resolveSashMachineIdentity(mode, userID).catch(() => null);
2130
+ const campaignsData = await fetchQoderCampaigns(accessToken, machineID, mode, identity);
2131
+ const campaigns = campaignsData.campaigns || [];
2132
+ const dailyCreditCampaign = campaigns.find(
2133
+ (c) => c.actionType === "CLAIM_BENEFIT" && (c.benefit?.kind === "CREDITS" || c.benefit?.amount === 100)
2134
+ ) || campaigns.find((c) => c.actionType === "CLAIM_BENEFIT");
2135
+ if (!dailyCreditCampaign) {
2136
+ if (wantsRaw) {
2137
+ const rawJson = JSON.stringify(campaignsData, null, 2);
2138
+ ctx?.ui?.notify(rawJson, "info");
2139
+ if (!ctx?.ui) console.log(rawJson);
2140
+ return;
2141
+ }
2142
+ const msg = identity === null ? mode === "cn" ? "\u26A0\uFE0F \u672A\u627E\u5230 Qoder \u684C\u9762\u7AEF\u8BBE\u5907\u6307\u7EB9\u7EC4\u4EF6\uFF08runtime-info.exe\uFF09\uFF0C\u670D\u52A1\u7AEF\u56E0\u6B64\u4E0D\u4E0B\u53D1\u53EF\u9886\u53D6\u7684\u7B7E\u5230\u6D3B\u52A8\u3002\u8BF7\u5B89\u88C5\u5E76\u6253\u5F00 Qoder \u684C\u9762\u7AEF\uFF0C\u6216\u8BBE\u7F6E QODER_UMID_EXE \u6307\u5411\u5176 resources/umid/runtime-info.exe \u540E\u91CD\u8BD5\u3002" : "\u26A0\uFE0F Daily check-in campaigns are hidden without desktop device attestation, and the Qoder desktop UMID component (runtime-info.exe) was not found on this machine.\n- Install/launch the Qoder desktop app, or set QODER_UMID_EXE to its resources/umid/runtime-info.exe, then retry." : mode === "cn" ? "\u2139\uFE0F \u5F53\u524D\u6682\u65E0\u53EF\u9886\u53D6\u7684\u7B7E\u5230\u6D3B\u52A8\uFF08\u6BCF\u65E5 10:00 UTC+8 \u5F00\u653E\u5237\u65B0\uFF09" : "\u2139\uFE0F Device verified, but no claimable check-in campaign is offered for this account/region right now (the campaign list carries banners only; claims reset at 10:00 UTC+8 when offered).";
2143
+ ctx?.ui?.notify(msg, identity === null ? "warning" : "info");
2144
+ if (!ctx?.ui) console.log(msg);
2145
+ return;
2146
+ }
2147
+ const content = mode === "cn" ? dailyCreditCampaign.placements?.[0]?.content?.zh || dailyCreditCampaign.placements?.[0]?.content?.en : dailyCreditCampaign.placements?.[0]?.content?.en || dailyCreditCampaign.placements?.[0]?.content?.zh;
2148
+ const campaignTitle = content?.title || (mode === "cn" ? "\u6BCF\u5929\u9886 100 Credits" : "Claim 100 Credits Daily");
2149
+ const amount = dailyCreditCampaign.benefit?.amount || 100;
2150
+ const validityDays = dailyCreditCampaign.benefit?.validity?.days || 30;
2151
+ const endAtMs = toEpochMs(dailyCreditCampaign.endAt);
2152
+ const remainingToReset = endAtMs && endAtMs > Date.now() ? endAtMs - Date.now() : msUntilBeijing10AM();
2153
+ const countdown = formatCountdownBeijing(remainingToReset);
2154
+ if (dailyCreditCampaign.claimStatus === "CLAIMED") {
2155
+ const cached = readCheckinCache(mode);
2156
+ writeCheckinCache(
2157
+ {
2158
+ claimed: true,
2159
+ amount,
2160
+ validityDays,
2161
+ campaignId: dailyCreditCampaign.campaignId,
2162
+ campaignTitle,
2163
+ claimedAt: cached?.claimedAt,
2164
+ expiresAt: cached?.expiresAt,
2165
+ cacheUntil: Date.now() + remainingToReset
2166
+ },
2167
+ mode
2168
+ );
2169
+ if (wantsRaw) {
2170
+ const rawJson = JSON.stringify(dailyCreditCampaign, null, 2);
2171
+ ctx?.ui?.notify(rawJson, "info");
2172
+ if (!ctx?.ui) console.log(rawJson);
2173
+ return;
2174
+ }
2175
+ const lines2 = mode === "cn" ? [
2176
+ paint(`\u2139\uFE0F \u4ECA\u65E5 ${amount} Credits \u5DF2\u7ECF\u9886\u53D6\u8FC7\uFF0C\u65E0\u9700\u91CD\u590D\u64CD\u4F5C`, ANSI.cyan),
2177
+ `- \u6D3B\u52A8\u540D\u79F0\uFF1A${campaignTitle}`,
2178
+ `- \u989D\u5EA6\u8BF4\u660E\uFF1A${amount} Credits\uFF08\u5168\u6A21\u578B\u901A\u7528\u8D44\u6E90\u5305\uFF0C${validityDays} \u5929\u6709\u6548\uFF09`,
2179
+ `- \u4E0B\u6B21\u5237\u65B0\uFF1A\u660E\u65E5 10:00 UTC+8\uFF08\u8DDD\u5237\u65B0\u7EA6 ${countdown}\uFF09`
2180
+ ] : [
2181
+ paint(`\u2139\uFE0F Today's ${amount} Credits already claimed.`, ANSI.cyan),
2182
+ `- Campaign: ${campaignTitle}`,
2183
+ `- Benefit: ${amount} Credits (${validityDays}-day validity)`,
2184
+ `- Next reset: Daily at 10:00 UTC+8 (in ~${countdown})`
2185
+ ];
2186
+ const output2 = lines2.join("\n");
2187
+ ctx?.ui?.notify(output2, "info");
2188
+ if (!ctx?.ui) console.log(output2);
2189
+ return;
2190
+ }
2191
+ const claimRes = await claimQoderCampaign(accessToken, machineID, dailyCreditCampaign.campaignId, mode, identity);
2192
+ const grantAmount = claimRes.benefit?.amount || amount;
2193
+ const grantDays = claimRes.benefit?.validity?.days || validityDays;
2194
+ const expiresText = claimRes.expiresAt ? formatDateTime(claimRes.expiresAt) : `${grantDays} \u5929\u540E`;
2195
+ writeCheckinCache(
2196
+ {
2197
+ claimed: true,
2198
+ amount: grantAmount,
2199
+ claimedAt: claimRes.claimedAt,
2200
+ expiresAt: claimRes.expiresAt,
2201
+ validityDays: grantDays,
2202
+ campaignId: dailyCreditCampaign.campaignId,
2203
+ campaignTitle,
2204
+ cacheUntil: Date.now() + remainingToReset
2205
+ },
2206
+ mode
2207
+ );
2208
+ const lines = mode === "cn" ? [
2209
+ paint(`\u{1F389} \u6210\u529F\u9886\u53D6\u4ECA\u65E5 ${grantAmount} Credits\uFF01`, `${ANSI.green}${ANSI.bold}`),
2210
+ `- \u989D\u5EA6\u7C7B\u578B\uFF1A\u5168\u6A21\u578B\u901A\u7528\u8D44\u6E90\u5305\uFF08Add-on Credits\uFF09`,
2211
+ `- \u6709\u6548\u671F\u9650\uFF1A${grantDays} \u5929\uFF08\u6709\u6548\u671F\u81F3 ${expiresText}\uFF09`,
2212
+ `- \u9886\u53D6\u6D41\u6C34\uFF1A${claimRes.grantId || "ok"}`,
2213
+ `- \u4E0B\u6B21\u5237\u65B0\uFF1A\u660E\u65E5 10:00 UTC+8\uFF08\u8DDD\u5237\u65B0\u7EA6 ${countdown}\uFF09`
2214
+ ] : [
2215
+ paint(`\u{1F389} Successfully claimed ${grantAmount} Credits!`, `${ANSI.green}${ANSI.bold}`),
2216
+ `- Type: Universal Add-on Credits`,
2217
+ `- Validity: ${grantDays} days (expires ${expiresText})`,
2218
+ `- Grant ID: ${claimRes.grantId || "ok"}`,
2219
+ `- Next reset: Daily at 10:00 UTC+8 (in ~${countdown})`
2220
+ ];
2221
+ const output = lines.join("\n");
2222
+ ctx?.ui?.notify(output, "info");
2223
+ if (!ctx?.ui) console.log(output);
2224
+ } catch (error) {
2225
+ const message = error instanceof Error ? error.message : String(error);
2226
+ const errText = mode === "cn" ? `\u274C \u6BCF\u65E5\u7B7E\u5230\u9886\u53D6\u5931\u8D25: ${message}` : `\u274C Daily check-in claim failed: ${message}`;
2227
+ ctx?.ui?.notify(errText, "error");
2228
+ if (!ctx?.ui) console.error(errText);
2229
+ }
2230
+ }
2231
+
1767
2232
  // src/protocol/errors.ts
2233
+ init_vpc();
1768
2234
  function parseQoderErrorPayload(payload) {
1769
2235
  if (!payload) return null;
1770
2236
  let parsed = payload;
@@ -1819,12 +2285,21 @@ function getBeijingDailyResetCountdown(now = Date.now()) {
1819
2285
  const minutes = totalMinutes % 60;
1820
2286
  return hours > 0 ? `${hours}\u5C0F\u65F6${minutes}\u5206\u949F` : `${minutes}\u5206\u949F`;
1821
2287
  }
1822
- function formatQoderStreamError(statusCode, rawBody, now = Date.now()) {
2288
+ function formatQoderStreamError(statusCode, rawBody, now = Date.now(), mode = "cn") {
1823
2289
  const parsed = parseQoderErrorPayload(rawBody);
1824
2290
  const code = parsed?.code;
1825
2291
  const message = parsed?.message || (typeof rawBody === "string" ? rawBody : JSON.stringify(rawBody));
1826
2292
  const countdown = getBeijingDailyResetCountdown(now);
2293
+ const isCn = mode !== "global";
1827
2294
  if (code === "110" || /billing daily count exceeded/i.test(message) || /daily usage limit reached/i.test(message)) {
2295
+ if (!isCn) {
2296
+ return [
2297
+ `[Qoder quota limit] Daily call allowance exhausted (Billing daily count exceeded, code 110)`,
2298
+ `- Reset: at 00:00 UTC+8 (in ${countdown})`,
2299
+ `- Reason: the account is on the free personal standard plan or tripped the daily frequency fuse. Add-on credits stay unspendable while the daily cap is active.`,
2300
+ `- Fix: wait for the reset; switch provider with /model; or upgrade at https://qoder.com/pricing to lift the daily limit.`
2301
+ ].join("\n");
2302
+ }
1828
2303
  return [
1829
2304
  `[Qoder CN \u989D\u5EA6\u9650\u5236] \u4ECA\u65E5\u8C03\u7528\u6B21\u6570\u5DF2\u8FBE\u4E0A\u9650 (Billing daily count exceeded, \u9519\u8BEF\u7801 110)`,
1830
2305
  `- \u5237\u65B0\u65F6\u95F4\uFF1A\u5C06\u5728\u5317\u4EAC\u65F6\u95F4 00:00 \u91CD\u7F6E\uFF08\u7EA6 ${countdown}\u540E\uFF09`,
@@ -1833,44 +2308,71 @@ function formatQoderStreamError(statusCode, rawBody, now = Date.now()) {
1833
2308
  ].join("\n");
1834
2309
  }
1835
2310
  if (code === "117" || /team member credits exhausted/i.test(message)) {
1836
- return [
2311
+ return isCn ? [
1837
2312
  `[Qoder CN \u4F01\u4E1A\u989D\u5EA6\u9650\u5236] \u4F01\u4E1A\u5206\u914D\u7ED9\u60A8\u7684\u4E2A\u4EBA Credits \u989D\u5EA6\u5DF2\u7528\u5C3D (\u9519\u8BEF\u7801 117)`,
1838
2313
  `- \u9650\u5236\u539F\u56E0\uFF1A\u7BA1\u7406\u5458\u5728\u4F01\u4E1A\u63A7\u5236\u53F0\u5206\u914D\u7ED9\u60A8\u4E2A\u4EBA\u7684\u53EF\u7528 Credits \u989D\u5EA6\u5DF2\u8017\u5C3D\u3002`,
1839
2314
  `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u8BF7\u8054\u7CFB\u4F01\u4E1A\u7BA1\u7406\u5458\u5728 Qoder \u56E2\u961F\u7BA1\u7406\u540E\u53F0\u4E3A\u60A8\u589E\u52A0\u6210\u5458 Credits \u914D\u989D\u3002`
2315
+ ].join("\n") : [
2316
+ `[Qoder enterprise quota] Your member credit allowance is exhausted (code 117)`,
2317
+ `- Reason: the admin-assigned per-member credit cap has been consumed.`,
2318
+ `- Fix: ask your organization admin to raise your member quota in the team console.`
1840
2319
  ].join("\n");
1841
2320
  }
1842
2321
  if (code === "116" || /team administrator credits exhausted/i.test(message)) {
1843
- return [
2322
+ return isCn ? [
1844
2323
  `[Qoder CN \u4F01\u4E1A\u989D\u5EA6\u9650\u5236] \u4F01\u4E1A/\u56E2\u961F\u7BA1\u7406\u5458\u7684 Credits \u603B\u4F59\u989D\u5DF2\u8017\u5C3D (\u9519\u8BEF\u7801 116)`,
1845
2324
  `- \u9650\u5236\u539F\u56E0\uFF1A\u5F53\u524D\u4F01\u4E1A\u7EC4\u7EC7\u8D26\u6237\u7684 Credits \u989D\u5EA6\u5DF2\u5168\u90E8\u7528\u5B8C\u3002`,
1846
2325
  `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u8BF7\u8054\u7CFB\u4F01\u4E1A\u7BA1\u7406\u5458\u5728\u63A7\u5236\u53F0\u4E3A\u7EC4\u7EC7\u8D26\u6237\u5145\u503C\u6216\u7EED\u671F\u3002`
2326
+ ].join("\n") : [
2327
+ `[Qoder enterprise quota] The organization's credit pool is exhausted (code 116)`,
2328
+ `- Reason: every credit in the organization account has been consumed.`,
2329
+ `- Fix: ask your organization admin to purchase or renew the org pool.`
1847
2330
  ].join("\n");
1848
2331
  }
1849
2332
  if (code === "122" || /billing-group credits limit reached/i.test(message)) {
1850
- return [
2333
+ return isCn ? [
1851
2334
  `[Qoder CN \u4F01\u4E1A\u8BA1\u8D39\u7EC4\u9650\u5236] \u60A8\u6240\u5728\u7684\u4F01\u4E1A\u8BA1\u8D39\u7EC4\u5DF2\u8FBE\u5230\u672C\u671F\u652F\u51FA\u4E0A\u9650 (\u9519\u8BEF\u7801 122)`,
1852
2335
  `- \u9650\u5236\u539F\u56E0\uFF1A\u8BA1\u8D39\u7EC4\u5468\u671F\u5185\u5DF2\u6D88\u8017\u5B8C\u7BA1\u7406\u5458\u8BBE\u5B9A\u7684\u4E0A\u9650\u989D\u5EA6\u3002`,
1853
2336
  `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u8BF7\u8054\u7CFB\u8BA1\u8D39\u7BA1\u7406\u5458\u6216\u4F01\u4E1A\u7BA1\u7406\u5458\u8C03\u6574\u8BE5\u8BA1\u8D39\u7EC4\u7684\u5468\u671F\u652F\u51FA\u4E0A\u9650\u3002`
2337
+ ].join("\n") : [
2338
+ `[Qoder billing group] The billing group reached its period spend cap (code 122)`,
2339
+ `- Reason: the admin-set limit for this billing group is consumed for the period.`,
2340
+ `- Fix: ask the billing administrator to raise the group's period cap.`
1854
2341
  ].join("\n");
1855
2342
  }
1856
2343
  if (code === "119" || /free usage limit for the selected model reached/i.test(message)) {
1857
- return [
2344
+ return isCn ? [
1858
2345
  `[Qoder CN \u6A21\u578B\u9650\u514D\u989D\u5EA6\u5DF2\u6EE1] \u5F53\u524D\u6A21\u578B\u7684\u514D\u8D39\u4F53\u9A8C\u989D\u5EA6\u5DF2\u7528\u5B8C (\u9519\u8BEF\u7801 119)`,
1859
2346
  `- \u5237\u65B0\u65F6\u95F4\uFF1A\u5C06\u5728\u5317\u4EAC\u65F6\u95F4 00:00 \u91CD\u7F6E\uFF08\u7EA6 ${countdown}\u540E\uFF09`,
1860
2347
  `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u53EF\u4F7F\u7528 /model \u5207\u6362\u81F3\u5176\u4ED6\u4ED8\u8D39\u6A21\u578B\uFF08\u5982 deepseek-v4-pro / glm-5.3\uFF09\u6D88\u8017 Credits \u989D\u5EA6\uFF0C\u6216\u6B21\u65E5\u91CD\u7F6E\u540E\u7EE7\u7EED\u4F7F\u7528\u3002`
2348
+ ].join("\n") : [
2349
+ `[Qoder model free tier] This model's free daily trial allowance is used up (code 119)`,
2350
+ `- Reset: at 00:00 UTC+8 (in ${countdown})`,
2351
+ `- Fix: switch to a paid model via /model to spend credits, or wait for the reset.`
1861
2352
  ].join("\n");
1862
2353
  }
1863
2354
  if (code === "113" || code === "118" || /quota exhausted/i.test(message) || /credits exhausted/i.test(message)) {
1864
- return [
2355
+ const store = isCn ? QODER_CN_OFFICIAL.manageUrl : "https://qoder.com";
2356
+ return isCn ? [
1865
2357
  `[Qoder CN \u989D\u5EA6\u8017\u5C3D] \u8D26\u6237 Credits \u4F59\u989D\u5DF2\u5168\u90E8\u7528\u5C3D (\u9519\u8BEF\u7801 ${code || 113})`,
1866
2358
  `- \u9650\u5236\u539F\u56E0\uFF1A\u5F53\u524D\u8BA2\u9605\u5957\u9910\u53CA\u8D44\u6E90\u5305\u5185\u7684\u53EF\u7528\u989D\u5EA6\u5DF2\u5168\u90E8\u6263\u51CF\u5B8C\u6BD5\u3002`,
1867
- `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u8BF7\u524D\u5F80 qoder.com.cn \u8D2D\u4E70\u8D44\u6E90\u5305/\u52A0\u6CB9\u5305\uFF0C\u6216\u7B49\u5F85\u4E0B\u4E00\u8BA1\u8D39\u5468\u671F\u5237\u65B0\u3002`
2359
+ `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u8BF7\u524D\u5F80 ${store} \u8D2D\u4E70\u8D44\u6E90\u5305/\u52A0\u6CB9\u5305\uFF0C\u6216\u7B49\u5F85\u4E0B\u4E00\u8BA1\u8D39\u5468\u671F\u5237\u65B0\u3002`
2360
+ ].join("\n") : [
2361
+ `[Qoder credits exhausted] The account has no remaining credits (code ${code || 113})`,
2362
+ `- Reason: the plan allowance and every add-on pack are consumed.`,
2363
+ `- Fix: purchase a credit pack at ${store}, or wait for the next billing cycle.`
1868
2364
  ].join("\n");
1869
2365
  }
1870
2366
  if (code === "105" || /token expired/i.test(message) || /login expired/i.test(message)) {
2367
+ if (isCn) {
2368
+ return [
2369
+ `[Qoder CN \u51ED\u8BC1\u5931\u6548] \u767B\u5F55\u6001\u5DF2\u8FC7\u671F\u6216 Token \u5931\u6548 (\u9519\u8BEF\u7801 105)`,
2370
+ `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u8BF7\u91CD\u65B0\u8FD0\u884C /login qoder-cn\uFF0C\u6216\u66F4\u65B0\u73AF\u5883\u53D8\u91CF\u4E2D\u7684\u4E2A\u4EBA\u8BBF\u95EE\u4EE4\u724C\uFF08PAT\uFF09\u3002`
2371
+ ].join("\n");
2372
+ }
1871
2373
  return [
1872
- `[Qoder CN \u51ED\u8BC1\u5931\u6548] \u767B\u5F55\u6001\u5DF2\u8FC7\u671F\u6216 Token \u5931\u6548 (\u9519\u8BEF\u7801 105)`,
1873
- `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u8BF7\u91CD\u65B0\u8FD0\u884C /login qoder-cn\uFF0C\u6216\u66F4\u65B0\u73AF\u5883\u53D8\u91CF\u4E2D\u7684\u4E2A\u4EBA\u8BBF\u95EE\u4EE4\u724C\uFF08PAT\uFF09\u3002`
2374
+ `[Qoder \u51ED\u8BC1\u5931\u6548 | Credential expired] The login session or token has expired (\u9519\u8BEF\u7801 105)`,
2375
+ `- Next step: run /login qoder. You can also refresh credentials by exporting QODER_PERSONAL_ACCESS_TOKEN (PAT).`
1874
2376
  ].join("\n");
1875
2377
  }
1876
2378
  if (rawBody === void 0 || rawBody === null) {
@@ -1880,10 +2382,40 @@ function formatQoderStreamError(statusCode, rawBody, now = Date.now()) {
1880
2382
  const truncated = bodyStr.length > 500 ? `${bodyStr.slice(0, 500)}...` : bodyStr;
1881
2383
  return `Upstream status ${statusCode}: ${truncated}`;
1882
2384
  }
2385
+ function isCredentialExpiredError(error) {
2386
+ const message = error instanceof Error ? error.message : String(error ?? "");
2387
+ return message.includes("\u9519\u8BEF\u7801 105") || message.includes("\u51ED\u8BC1\u5931\u6548") || /token expired|login expired/i.test(message);
2388
+ }
1883
2389
 
1884
2390
  // src/commands/usage.ts
1885
2391
  init_region();
1886
- var ANSI = {
2392
+ function estimateRollingAddOnExpiry(addOn, latestExpiryMs, now = Date.now()) {
2393
+ if (!addOn || !addOn.total || addOn.total <= 0 || !addOn.remaining || addOn.remaining <= 0) {
2394
+ return null;
2395
+ }
2396
+ const packSize = 100;
2397
+ const totalPacks = Math.max(1, Math.round(addOn.total / packSize));
2398
+ if (totalPacks > 50) return null;
2399
+ const used = Math.max(0, addOn.used ?? 0);
2400
+ const consumedPacks = Math.min(totalPacks - 1, Math.floor(used / packSize));
2401
+ const earliestActiveIndex = consumedPacks;
2402
+ const daysAgoClaimed = totalPacks - 1 - earliestActiveIndex;
2403
+ const anchorExpiryMs = latestExpiryMs && Number.isFinite(latestExpiryMs) ? latestExpiryMs : now + 30 * 864e5;
2404
+ const earliestMs = anchorExpiryMs - daysAgoClaimed * 864e5;
2405
+ const daysRemaining = Math.max(0, Math.round((earliestMs - now) / 864e5));
2406
+ const earliestRemaining = Math.min(addOn.remaining, packSize - used % packSize);
2407
+ const d = new Date(earliestMs);
2408
+ const earliestDate = d.toISOString().slice(0, 10);
2409
+ return {
2410
+ earliestDate,
2411
+ earliestMs,
2412
+ earliestRemaining,
2413
+ totalPacks,
2414
+ consumedPacks,
2415
+ daysRemaining
2416
+ };
2417
+ }
2418
+ var ANSI2 = {
1887
2419
  reset: "\x1B[0m",
1888
2420
  bold: "\x1B[1m",
1889
2421
  dim: "\x1B[2m",
@@ -1896,15 +2428,15 @@ var EMPTY_CHAR = "\u2591";
1896
2428
  var UNKNOWN_CHAR = "?";
1897
2429
  var BAR_WIDTH = 20;
1898
2430
  var COLUMN_GAP = " ";
1899
- function paint(text, code) {
1900
- return code && text ? `${code}${text}${ANSI.reset}` : text;
2431
+ function paint2(text, code) {
2432
+ return code && text ? `${code}${text}${ANSI2.reset}` : text;
1901
2433
  }
1902
2434
  function usageColor(percentage, danger = false) {
1903
- if (danger) return `${ANSI.red}${ANSI.bold}`;
2435
+ if (danger) return `${ANSI2.red}${ANSI2.bold}`;
1904
2436
  if (typeof percentage !== "number" || !Number.isFinite(percentage)) return void 0;
1905
- if (percentage >= 85) return `${ANSI.red}${ANSI.bold}`;
1906
- if (percentage >= 60) return ANSI.yellow;
1907
- return ANSI.green;
2437
+ if (percentage >= 85) return `${ANSI2.red}${ANSI2.bold}`;
2438
+ if (percentage >= 60) return ANSI2.yellow;
2439
+ return ANSI2.green;
1908
2440
  }
1909
2441
  function usageBar(percentage, options = {}) {
1910
2442
  const width = options.width ?? BAR_WIDTH;
@@ -1913,7 +2445,7 @@ function usageBar(percentage, options = {}) {
1913
2445
  const fill = known ? FILLED_CHAR.repeat(filled) : "";
1914
2446
  const rest = known ? EMPTY_CHAR.repeat(width - filled) : UNKNOWN_CHAR.repeat(width);
1915
2447
  if (!options.color) return `[${fill}${rest}]`;
1916
- return paint("[", ANSI.dim) + paint(fill, usageColor(percentage, options.danger)) + paint(rest, ANSI.dim) + paint("]", ANSI.dim);
2448
+ return paint2("[", ANSI2.dim) + paint2(fill, usageColor(percentage, options.danger)) + paint2(rest, ANSI2.dim) + paint2("]", ANSI2.dim);
1917
2449
  }
1918
2450
  function formatAmount(value, unit) {
1919
2451
  if (typeof value !== "number" || !Number.isFinite(value)) return "?";
@@ -1950,7 +2482,7 @@ function isBucketRow(row) {
1950
2482
  return typeof row.used === "number" || typeof row.total === "number";
1951
2483
  }
1952
2484
  function rightCell(text, width, code) {
1953
- return " ".repeat(Math.max(0, width - text.length)) + paint(text, code);
2485
+ return " ".repeat(Math.max(0, width - text.length)) + paint2(text, code);
1954
2486
  }
1955
2487
  function renderUsageRows(rows, notes, color) {
1956
2488
  const buckets = rows.filter(isBucketRow);
@@ -1975,7 +2507,7 @@ function renderUsageRows(rows, notes, color) {
1975
2507
  ].join(COLUMN_GAP);
1976
2508
  });
1977
2509
  for (const note of notes) {
1978
- lines.push(`${note.label.padEnd(labelWidth)}${COLUMN_GAP}${color ? paint(note.value, note.color) : note.value}`);
2510
+ lines.push(`${note.label.padEnd(labelWidth)}${COLUMN_GAP}${color ? paint2(note.value, note.color) : note.value}`);
1979
2511
  }
1980
2512
  return lines.map((line) => line.replace(/ +$/, ""));
1981
2513
  }
@@ -2023,7 +2555,7 @@ function formatQoderUsage(raw, mode, now = Date.now(), options = {}) {
2023
2555
  const isPersonalStandard = raw?.userType === "personal_standard" || raw?.userType === "free";
2024
2556
  const notes = [];
2025
2557
  if (danger) {
2026
- notes.push({ label: "Status", value: "quota exceeded", color: ANSI.red });
2558
+ notes.push({ label: "Status", value: "quota exceeded", color: ANSI2.red });
2027
2559
  }
2028
2560
  if (!showUserQuota && !showAddOnQuota && !showOrg) notes.push({ label: "Buckets", value: "none returned" });
2029
2561
  if (raw?.isPlanQuotaProrated) notes.push({ label: "Note", value: "plan quota is prorated" });
@@ -2034,13 +2566,48 @@ function formatQoderUsage(raw, mode, now = Date.now(), options = {}) {
2034
2566
  if (isPersonalStandard || !showOrg && formatResetTime(raw?.expiresAt, now) === "never") {
2035
2567
  notes.push({ label: "Daily reset", value: formatDailyResetCountdown(now) });
2036
2568
  }
2569
+ const checkin = options.checkin;
2570
+ if (checkin) {
2571
+ if (checkin.claimed && checkin.expiresAt) {
2572
+ const rel = formatResetTime(Date.parse(checkin.expiresAt), now);
2573
+ notes.push({
2574
+ label: "Today checkin",
2575
+ value: `${checkin.amount} Credits claimed (expires ${rel})`,
2576
+ color: color ? ANSI2.green : void 0
2577
+ });
2578
+ } else if (checkin.claimed) {
2579
+ notes.push({
2580
+ label: "Today checkin",
2581
+ value: `${checkin.amount} Credits claimed (expires in ~30d)`,
2582
+ color: color ? ANSI2.green : void 0
2583
+ });
2584
+ } else {
2585
+ notes.push({
2586
+ label: "Today checkin",
2587
+ value: `${checkin.amount} Credits available (run /${region.providerID}.claim)`,
2588
+ color: color ? ANSI2.yellow : void 0
2589
+ });
2590
+ }
2591
+ }
2592
+ const addOn = pickBucket(raw, "addOnQuota");
2593
+ if (showAddOnQuota && (addOn?.remaining ?? 0) > 0 && checkin) {
2594
+ const latestExpiryMs = checkin.expiresAt ? Date.parse(checkin.expiresAt) : void 0;
2595
+ const estimate = estimateRollingAddOnExpiry(addOn, latestExpiryMs, now);
2596
+ if (estimate) {
2597
+ const earliestText = estimate.totalPacks > 1 ? `earliest active pack ~${estimate.earliestDate} \xB7 in ~${estimate.daysRemaining}d (~${formatAmount(estimate.earliestRemaining)} credits)` : `${estimate.earliestDate} (in ${estimate.daysRemaining}d)`;
2598
+ notes.push({
2599
+ label: "Add-on expiry",
2600
+ value: `Rolling 30d (${earliestText})`
2601
+ });
2602
+ }
2603
+ }
2037
2604
  notes.push({ label: "Expires", value: formatResetTime(raw?.expiresAt, now) });
2038
2605
  const manageUrl = raw?.addOnQuota?.detailUrl || raw?.upgradeUrl || region.manageUrl;
2039
2606
  if (manageUrl) notes.push({ label: "Manage", value: manageUrl });
2040
2607
  const title = `${region.usageTitle}${userType}`;
2041
2608
  return {
2042
2609
  title: region.usageTitle,
2043
- lines: [paint(title, color ? ANSI.bold : void 0), ...renderUsageRows(rows, notes, color)]
2610
+ lines: [paint2(title, color ? ANSI2.bold : void 0), ...renderUsageRows(rows, notes, color)]
2044
2611
  };
2045
2612
  }
2046
2613
  async function fetchQoderQuota(accessToken, mode) {
@@ -2050,7 +2617,8 @@ async function fetchQoderQuota(accessToken, mode) {
2050
2617
  Authorization: `Bearer ${accessToken}`,
2051
2618
  Accept: "application/json",
2052
2619
  "User-Agent": "pi-provider-qoder-cn"
2053
- }
2620
+ },
2621
+ signal: AbortSignal.timeout(1e4)
2054
2622
  });
2055
2623
  const text = await response.text();
2056
2624
  let data;
@@ -2091,7 +2659,7 @@ async function resolveUsageIdentity(providerID, mode, ctx, withIdentity = true)
2091
2659
  } catch {
2092
2660
  }
2093
2661
  }
2094
- return { access, name, email };
2662
+ return { access, name, email, userID: stored?.userID || (storedMatches ? stored?.userID : void 0) };
2095
2663
  }
2096
2664
  var FORMAT_ARGS = /* @__PURE__ */ new Set(["json", "raw", "debug"]);
2097
2665
  var PLAIN_ARGS = /* @__PURE__ */ new Set(["plain", "no-color", "nocolor"]);
@@ -2113,9 +2681,15 @@ async function runUsageCommand(mode, args, ctx) {
2113
2681
  ctx?.ui?.notify(`No ${providerID} credentials. Run /login ${providerID} first.`, "warning");
2114
2682
  return;
2115
2683
  }
2116
- const raw = await fetchQoderQuota(accessToken, mode);
2684
+ const { getMachineId: getMachineId2 } = await Promise.resolve().then(() => (init_cosy(), cosy_exports));
2685
+ const machineID = getMachineId2();
2686
+ const deviceIdentity = wantsRaw ? null : await resolveSashMachineIdentity(mode, identity?.userID).catch(() => null);
2687
+ const [raw, checkinInfo] = await Promise.all([
2688
+ fetchQoderQuota(accessToken, mode),
2689
+ wantsRaw ? Promise.resolve(null) : fetchCheckinGrantInfo(accessToken, machineID, mode, deviceIdentity).catch(() => null)
2690
+ ]);
2117
2691
  const color = !wantsRaw && shouldColorize(args, { hasUI: Boolean(ctx?.ui) });
2118
- const output = wantsRaw ? JSON.stringify(raw, null, 2) : formatQoderUsage(raw, mode, Date.now(), { color, user: identity }).lines.join("\n");
2692
+ const output = wantsRaw ? JSON.stringify(raw, null, 2) : formatQoderUsage(raw, mode, Date.now(), { color, user: identity, checkin: checkinInfo }).lines.join("\n");
2119
2693
  ctx?.ui?.notify(output, "info");
2120
2694
  if (!ctx?.ui) console.log(output);
2121
2695
  } catch (error) {
@@ -2575,6 +3149,8 @@ function transformMessagesForQoder(messages) {
2575
3149
  }
2576
3150
 
2577
3151
  // src/protocol/stream.ts
3152
+ var healCooldowns = /* @__PURE__ */ new Map();
3153
+ var HEAL_COOLDOWN_MS = 6e4;
2578
3154
  function stableHash(prefix, ...inputs) {
2579
3155
  const hash = crypto3.createHash("sha256");
2580
3156
  hash.update(prefix);
@@ -2661,6 +3237,25 @@ async function peekQueueNotice(reader, decoder) {
2661
3237
  return null;
2662
3238
  }
2663
3239
  }
3240
+ async function peekCredentialExpired(reader, decoder) {
3241
+ const text = await reader.peekLine(decoder);
3242
+ const firstLine = text.split("\n")[0]?.trim() ?? "";
3243
+ if (!firstLine.startsWith("data:")) return false;
3244
+ const payload = firstLine.slice(5).trim();
3245
+ if (!payload || payload === "[DONE]") return false;
3246
+ try {
3247
+ const envelope = JSON.parse(payload);
3248
+ if (envelope.isQueued === true) return false;
3249
+ if (typeof envelope.statusCodeValue === "number" && envelope.statusCodeValue !== 200) {
3250
+ const parsed = parseQoderErrorPayload(envelope.body);
3251
+ const message = parsed?.message ?? (typeof envelope.body === "string" ? envelope.body : "");
3252
+ return parsed?.code === "105" || /token expired|login expired/i.test(message);
3253
+ }
3254
+ return false;
3255
+ } catch {
3256
+ return false;
3257
+ }
3258
+ }
2664
3259
  function resolveRequestContext(piAi, context) {
2665
3260
  const {
2666
3261
  collapseSystemMessages,
@@ -2710,374 +3305,424 @@ function streamQoder(model, context, options) {
2710
3305
  (async () => {
2711
3306
  try {
2712
3307
  const providerMode = model.provider === "qoder-cn" ? "cn" : "global";
3308
+ let reader;
3309
+ let started = false;
2713
3310
  const region = getQoderRegionConfig(providerMode);
2714
- const accessToken = options?.apiKey;
2715
- if (!accessToken) {
2716
- throw new Error(
2717
- providerMode === "cn" ? "Qoder CN credentials not set. Run /login qoder-cn or set QODERCN_PERSONAL_ACCESS_TOKEN." : "Qoder credentials not set. Run /login qoder or set QODER_PERSONAL_ACCESS_TOKEN."
2718
- );
2719
- }
2720
- const ident = await resolveQoderIdentity(accessToken, model.provider, providerMode);
2721
- const userID = ident.userID || "qoder-user";
2722
- const name = ident.name || region.userNameFallback;
2723
- const email = ident.email || region.userEmailFallback;
2724
- const machineID = ident.machineID || getMachineId();
2725
- const modelConfig = getCachedModelConfig(model.id, providerMode);
2726
- if (!modelConfig?.key) {
2727
- throw new Error(`Unknown Qoder model id: ${model.id}`);
2728
- }
2729
- const qoderModel = modelConfig.key;
2730
- const isReasoning = !!modelConfig.is_reasoning;
2731
- const resolved = resolveRequestContext(PiAi, context);
2732
- const normalizedMessages = transformMessagesForQoder(resolved.messages);
2733
- const systemText = resolved.systemText;
2734
- let lastUserText = "";
2735
- for (let i = normalizedMessages.length - 1; i >= 0; i--) {
2736
- if (normalizedMessages[i].role === "user") {
2737
- const content = normalizedMessages[i].content;
2738
- lastUserText = typeof content === "string" ? content : Array.isArray(content) ? content.map((c) => "text" in c ? c.text : "").join("") : "";
2739
- break;
3311
+ const runAttempt = async (apiKey) => {
3312
+ const accessToken = apiKey ?? options?.apiKey;
3313
+ if (!accessToken) {
3314
+ throw new Error(
3315
+ providerMode === "cn" ? "Qoder CN credentials not set. Run /login qoder-cn or set QODERCN_PERSONAL_ACCESS_TOKEN." : "Qoder credentials not set. Run /login qoder or set QODER_PERSONAL_ACCESS_TOKEN."
3316
+ );
2740
3317
  }
2741
- }
2742
- const stablePart = stableHash("qoder-session", userID, qoderModel);
2743
- const sessionID = options?.sessionId ? `${stablePart}-${options.sessionId}` : `${stablePart}-${crypto3.randomUUID()}`;
2744
- let maxTokens = MAX_OUTPUT_TOKENS;
2745
- if (options?.maxTokens && options.maxTokens < maxTokens) {
2746
- maxTokens = options.maxTokens;
2747
- }
2748
- const toolsRaw = resolved.tools.length > 0 ? transformTools(resolved.tools) : void 0;
2749
- const recordID = stableChatRecordID(qoderModel, normalizedMessages, toolsRaw, maxTokens);
2750
- const requestedLevel = options?.reasoning;
2751
- const clamped = requestedLevel ? clampThinkingLevel(model, requestedLevel) : void 0;
2752
- const reasoningLevel = clamped === "off" ? void 0 : clamped;
2753
- const parameters = { max_tokens: maxTokens };
2754
- if (reasoningLevel) {
2755
- parameters.enable_thinking = true;
2756
- const mapped = model.thinkingLevelMap?.[reasoningLevel];
2757
- const effort = mapped && mapped !== "enabled" && mapped !== "disabled" ? mapped : reasoningLevel;
2758
- if (modelConfig?.thinking_config?.enabled?.efforts && typeof effort === "string") {
2759
- parameters.reasoning_effort = effort;
3318
+ const ident = await resolveQoderIdentity(accessToken, model.provider, providerMode);
3319
+ const userID = ident.userID || "qoder-user";
3320
+ const name = ident.name || region.userNameFallback;
3321
+ const email = ident.email || region.userEmailFallback;
3322
+ const machineID = ident.machineID || getMachineId();
3323
+ const modelConfig = getCachedModelConfig(model.id, providerMode);
3324
+ if (!modelConfig?.key) {
3325
+ throw new Error(`Unknown Qoder model id: ${model.id}`);
2760
3326
  }
2761
- } else {
2762
- parameters.enable_thinking = false;
2763
- }
2764
- const reqBody = {
2765
- request_id: crypto3.randomUUID(),
2766
- request_set_id: recordID,
2767
- chat_record_id: recordID,
2768
- session_id: sessionID,
2769
- stream: true,
2770
- chat_task: "FREE_INPUT",
2771
- is_reply: true,
2772
- is_retry: false,
2773
- source: 1,
2774
- version: "3",
2775
- session_type: "qodercli",
2776
- agent_id: "agent_common",
2777
- task_id: "common",
2778
- code_language: "",
2779
- chat_prompt: "",
2780
- image_urls: null,
2781
- aliyun_user_type: "",
2782
- // Qoder's server ignores the top-level `system` field (verified: the
2783
- // model never sees it). Inject the system prompt as a leading
2784
- // role:system message instead, which the server does honor.
2785
- system: "",
2786
- messages: systemText ? [{ role: "system", content: systemText }, ...normalizedMessages] : normalizedMessages,
2787
- tools: toolsRaw || [],
2788
- parameters,
2789
- chat_context: {
2790
- chatPrompt: "",
2791
- imageUrls: null,
2792
- extra: {
2793
- context: [],
2794
- modelConfig: {
2795
- key: qoderModel,
2796
- is_reasoning: isReasoning
2797
- },
2798
- originalContent: lastUserText
2799
- },
2800
- features: [],
2801
- text: lastUserText
2802
- },
2803
- model_config: modelConfig,
2804
- business: {
2805
- product: "cli",
2806
- version: "1.0.0",
2807
- type: "agent",
2808
- stage: "start",
2809
- id: crypto3.randomUUID(),
2810
- name: lastUserText.substring(0, 30),
2811
- begin_at: Date.now()
3327
+ const qoderModel = modelConfig.key;
3328
+ const isReasoning = !!modelConfig.is_reasoning;
3329
+ const resolved = resolveRequestContext(PiAi, context);
3330
+ const normalizedMessages = transformMessagesForQoder(resolved.messages);
3331
+ const systemText = resolved.systemText;
3332
+ let lastUserText = "";
3333
+ for (let i = normalizedMessages.length - 1; i >= 0; i--) {
3334
+ if (normalizedMessages[i].role === "user") {
3335
+ const content = normalizedMessages[i].content;
3336
+ lastUserText = typeof content === "string" ? content : Array.isArray(content) ? content.map((c) => "text" in c ? c.text : "").join("") : "";
3337
+ break;
3338
+ }
2812
3339
  }
2813
- };
2814
- const bodyBytes = Buffer.from(JSON.stringify(reqBody));
2815
- const encodedBytes = qoderEncodeBody(bodyBytes);
2816
- const chatURL = getQoderChatURL(providerMode);
2817
- const headers = buildAuthHeaders(encodedBytes, chatURL, {
2818
- userID,
2819
- authToken: accessToken,
2820
- name,
2821
- email,
2822
- machineID
2823
- });
2824
- const modelSource = modelConfig.source || "system";
2825
- let queuedAttempt = 0;
2826
- let reader;
2827
- let decoder;
2828
- let buffer = "";
2829
- let bufferStart = 0;
2830
- for (; ; ) {
2831
- const response = await fetch(chatURL, {
2832
- method: "POST",
2833
- headers: {
2834
- "Content-Type": "application/json",
2835
- Accept: "text/event-stream",
2836
- "Cache-Control": "no-cache",
2837
- "Accept-Encoding": "identity",
2838
- "X-Model-Key": qoderModel,
2839
- "X-Model-Source": modelSource,
2840
- ...headers
3340
+ const stablePart = stableHash("qoder-session", userID, qoderModel);
3341
+ const sessionID = options?.sessionId ? `${stablePart}-${options.sessionId}` : `${stablePart}-${crypto3.randomUUID()}`;
3342
+ let maxTokens = MAX_OUTPUT_TOKENS;
3343
+ if (options?.maxTokens && options.maxTokens < maxTokens) {
3344
+ maxTokens = options.maxTokens;
3345
+ }
3346
+ const toolsRaw = resolved.tools.length > 0 ? transformTools(resolved.tools) : void 0;
3347
+ const recordID = stableChatRecordID(qoderModel, normalizedMessages, toolsRaw, maxTokens);
3348
+ const requestedLevel = options?.reasoning;
3349
+ const clamped = requestedLevel ? clampThinkingLevel(model, requestedLevel) : void 0;
3350
+ const reasoningLevel = clamped === "off" ? void 0 : clamped;
3351
+ const parameters = { max_tokens: maxTokens };
3352
+ if (reasoningLevel) {
3353
+ parameters.enable_thinking = true;
3354
+ const mapped = model.thinkingLevelMap?.[reasoningLevel];
3355
+ const effort = mapped && mapped !== "enabled" && mapped !== "disabled" ? mapped : reasoningLevel;
3356
+ if (modelConfig?.thinking_config?.enabled?.efforts && typeof effort === "string") {
3357
+ parameters.reasoning_effort = effort;
3358
+ }
3359
+ } else {
3360
+ parameters.enable_thinking = false;
3361
+ }
3362
+ const reqBody = {
3363
+ request_id: crypto3.randomUUID(),
3364
+ request_set_id: recordID,
3365
+ chat_record_id: recordID,
3366
+ session_id: sessionID,
3367
+ stream: true,
3368
+ chat_task: "FREE_INPUT",
3369
+ is_reply: true,
3370
+ is_retry: false,
3371
+ source: 1,
3372
+ version: "3",
3373
+ session_type: "qodercli",
3374
+ agent_id: "agent_common",
3375
+ task_id: "common",
3376
+ code_language: "",
3377
+ chat_prompt: "",
3378
+ image_urls: null,
3379
+ aliyun_user_type: "",
3380
+ // Qoder's server ignores the top-level `system` field (verified: the
3381
+ // model never sees it). Inject the system prompt as a leading
3382
+ // role:system message instead, which the server does honor.
3383
+ system: "",
3384
+ messages: systemText ? [{ role: "system", content: systemText }, ...normalizedMessages] : normalizedMessages,
3385
+ tools: toolsRaw || [],
3386
+ parameters,
3387
+ chat_context: {
3388
+ chatPrompt: "",
3389
+ imageUrls: null,
3390
+ extra: {
3391
+ context: [],
3392
+ modelConfig: {
3393
+ key: qoderModel,
3394
+ is_reasoning: isReasoning
3395
+ },
3396
+ originalContent: lastUserText
3397
+ },
3398
+ features: [],
3399
+ text: lastUserText
2841
3400
  },
2842
- body: encodedBytes,
2843
- signal: options?.signal
3401
+ model_config: modelConfig,
3402
+ business: {
3403
+ product: "cli",
3404
+ version: "1.0.0",
3405
+ type: "agent",
3406
+ stage: "start",
3407
+ id: crypto3.randomUUID(),
3408
+ name: lastUserText.substring(0, 30),
3409
+ begin_at: Date.now()
3410
+ }
3411
+ };
3412
+ const bodyBytes = Buffer.from(JSON.stringify(reqBody));
3413
+ const encodedBytes = qoderEncodeBody(bodyBytes);
3414
+ const chatURL = getQoderChatURL(providerMode);
3415
+ const headers = buildAuthHeaders(encodedBytes, chatURL, {
3416
+ userID,
3417
+ authToken: accessToken,
3418
+ name,
3419
+ email,
3420
+ machineID
2844
3421
  });
2845
- if (!response.ok) {
2846
- const errText = await response.text();
2847
- throw new Error(formatQoderStreamError(response.status, errText));
2848
- }
2849
- const body = response.body;
2850
- if (!body) throw new Error("No response body");
2851
- reader = new PeekableReader(body.getReader());
2852
- decoder = new TextDecoder();
2853
- buffer = "";
2854
- bufferStart = 0;
2855
- const queue = await peekQueueNotice(reader, decoder);
2856
- if (queue) {
2857
- await reader.cancel().catch(() => {
3422
+ const modelSource = modelConfig.source || "system";
3423
+ let queuedAttempt = 0;
3424
+ await reader?.cancel().catch(() => {
3425
+ });
3426
+ reader = void 0;
3427
+ let decoder;
3428
+ let buffer = "";
3429
+ let bufferStart = 0;
3430
+ for (; ; ) {
3431
+ const response = await fetch(chatURL, {
3432
+ method: "POST",
3433
+ headers: {
3434
+ "Content-Type": "application/json",
3435
+ Accept: "text/event-stream",
3436
+ "Cache-Control": "no-cache",
3437
+ "Accept-Encoding": "identity",
3438
+ "X-Model-Key": qoderModel,
3439
+ "X-Model-Source": modelSource,
3440
+ ...headers
3441
+ },
3442
+ body: encodedBytes,
3443
+ signal: options?.signal
2858
3444
  });
2859
- if (queuedAttempt >= MAX_QUEUE_RETRIES) {
3445
+ if (!response.ok) {
3446
+ const errText = await response.text();
3447
+ throw new Error(formatQoderStreamError(response.status, errText, Date.now(), providerMode));
3448
+ }
3449
+ const body = response.body;
3450
+ if (!body) throw new Error("No response body");
3451
+ reader = new PeekableReader(body.getReader());
3452
+ decoder = new TextDecoder();
3453
+ buffer = "";
3454
+ bufferStart = 0;
3455
+ const queue = await peekQueueNotice(reader, decoder);
3456
+ if (queue) {
3457
+ await reader.cancel().catch(() => {
3458
+ });
3459
+ if (queuedAttempt >= MAX_QUEUE_RETRIES) {
3460
+ throw new Error(
3461
+ `Qoder is at capacity: the request stayed queued after ${queuedAttempt + 1} attempts (~${queue.waitTimeSeconds}s wait reported). Try again shortly.`
3462
+ );
3463
+ }
3464
+ queuedAttempt++;
3465
+ await sleep(queue.retryAfterMs, options?.signal);
3466
+ continue;
3467
+ }
3468
+ if (await peekCredentialExpired(reader, decoder)) {
3469
+ await reader.cancel().catch(() => {
3470
+ });
2860
3471
  throw new Error(
2861
- `Qoder is at capacity: the request stayed queued after ${queuedAttempt + 1} attempts (~${queue.waitTimeSeconds}s wait reported). Try again shortly.`
3472
+ formatQoderStreamError(403, '{"code":"105","message":"Login expired"}', Date.now(), providerMode)
2862
3473
  );
2863
3474
  }
2864
- queuedAttempt++;
2865
- await sleep(queue.retryAfterMs, options?.signal);
2866
- continue;
3475
+ break;
2867
3476
  }
2868
- break;
2869
- }
2870
- if (!reader || !decoder) throw new Error("No response body");
2871
- let contentBlockIndex = -1;
2872
- let thinkingBlockIndex = -1;
2873
- let rawReasoningTail = "";
2874
- const toolCallsState = [];
2875
- const thinkingEnabled = options?.reasoning !== false && options?.reasoning !== "off";
2876
- const thinkingParser = thinkingEnabled ? new ThinkingTagParser(output, stream) : null;
2877
- stream.push({ type: "start", partial: output });
2878
- let sawDone = false;
2879
- while (!sawDone) {
2880
- const { done, value } = await reader.read();
2881
- if (done) break;
2882
- if (bufferStart > 0) {
2883
- buffer = buffer.substring(bufferStart);
2884
- bufferStart = 0;
3477
+ if (!reader || !decoder) throw new Error("No response body");
3478
+ let contentBlockIndex = -1;
3479
+ let thinkingBlockIndex = -1;
3480
+ let rawReasoningTail = "";
3481
+ const toolCallsState = [];
3482
+ const thinkingEnabled = options?.reasoning !== false && options?.reasoning !== "off";
3483
+ const thinkingParser = thinkingEnabled ? new ThinkingTagParser(output, stream) : null;
3484
+ if (!started) {
3485
+ started = true;
3486
+ } else {
3487
+ output.content.length = 0;
2885
3488
  }
2886
- buffer += decoder.decode(value, { stream: true });
2887
- while (true) {
2888
- const lineEnd = buffer.indexOf("\n", bufferStart);
2889
- if (lineEnd === -1) break;
2890
- const line = buffer.substring(bufferStart, lineEnd).trim();
2891
- bufferStart = lineEnd + 1;
2892
- if (!line.startsWith("data:")) continue;
2893
- const dataStr = line.substring(5).trim();
2894
- if (dataStr === "[DONE]") {
2895
- sawDone = true;
2896
- break;
3489
+ stream.push({ type: "start", partial: output });
3490
+ let sawDone = false;
3491
+ while (!sawDone) {
3492
+ const { done, value } = await reader.read();
3493
+ if (done) break;
3494
+ if (bufferStart > 0) {
3495
+ buffer = buffer.substring(bufferStart);
3496
+ bufferStart = 0;
2897
3497
  }
2898
- try {
2899
- const envelope = JSON.parse(dataStr);
2900
- if (envelope.statusCodeValue && envelope.statusCodeValue !== 200) {
2901
- throw new Error(formatQoderStreamError(envelope.statusCodeValue, envelope.body));
2902
- }
2903
- const innerStr = envelope.body;
2904
- if (innerStr === "[DONE]") {
3498
+ buffer += decoder.decode(value, { stream: true });
3499
+ while (true) {
3500
+ const lineEnd = buffer.indexOf("\n", bufferStart);
3501
+ if (lineEnd === -1) break;
3502
+ const line = buffer.substring(bufferStart, lineEnd).trim();
3503
+ bufferStart = lineEnd + 1;
3504
+ if (!line.startsWith("data:")) continue;
3505
+ const dataStr = line.substring(5).trim();
3506
+ if (dataStr === "[DONE]") {
2905
3507
  sawDone = true;
2906
3508
  break;
2907
3509
  }
2908
- if (!innerStr) continue;
2909
- const inner = JSON.parse(innerStr);
2910
- if (inner.id) output.responseId = inner.id;
2911
- if (inner.model) output.responseModel = inner.model;
2912
- if (inner.usage) {
2913
- const u = inner.usage;
2914
- const promptTokens = u.prompt_tokens ?? 0;
2915
- const cacheReadTokens = u.prompt_tokens_details?.cached_tokens ?? 0;
2916
- const cacheWriteTokens = u.prompt_tokens_details?.cache_write_tokens ?? 0;
2917
- output.usage.input = Math.max(0, promptTokens - cacheReadTokens - cacheWriteTokens);
2918
- output.usage.output = u.completion_tokens ?? 0;
2919
- output.usage.totalTokens = u.total_tokens ?? 0;
2920
- output.usage.cacheRead = cacheReadTokens;
2921
- output.usage.cacheWrite = cacheWriteTokens;
2922
- }
2923
- if (inner.choices && inner.choices.length > 0) {
2924
- const choice = inner.choices[0];
2925
- const delta = choice.delta;
2926
- if (delta) {
2927
- if (delta.reasoning_content) {
2928
- rawReasoningTail = (rawReasoningTail + delta.reasoning_content).slice(-512);
2929
- const reasoningChunk = stripDsmlResidue(stripThinkingTags(delta.reasoning_content));
2930
- if (reasoningChunk) {
2931
- if (thinkingBlockIndex === -1) {
2932
- thinkingBlockIndex = output.content.length;
2933
- output.content.push({ type: "thinking", thinking: "" });
2934
- stream.push({ type: "thinking_start", contentIndex: thinkingBlockIndex, partial: output });
2935
- }
2936
- const block = output.content[thinkingBlockIndex];
2937
- block.thinking += reasoningChunk;
2938
- stream.push({
2939
- type: "thinking_delta",
2940
- contentIndex: thinkingBlockIndex,
2941
- delta: reasoningChunk,
2942
- partial: output
2943
- });
2944
- }
2945
- }
2946
- if (delta.content) {
2947
- if (thinkingBlockIndex !== -1) {
2948
- const block = output.content[thinkingBlockIndex];
2949
- stream.push({
2950
- type: "thinking_end",
2951
- contentIndex: thinkingBlockIndex,
2952
- content: block.thinking,
2953
- partial: output
2954
- });
2955
- thinkingBlockIndex = -1;
2956
- }
2957
- if (thinkingParser) {
2958
- thinkingParser.processChunk(delta.content);
2959
- } else {
2960
- if (contentBlockIndex === -1) {
2961
- contentBlockIndex = output.content.length;
2962
- output.content.push({ type: "text", text: "" });
2963
- stream.push({ type: "text_start", contentIndex: contentBlockIndex, partial: output });
3510
+ try {
3511
+ const envelope = JSON.parse(dataStr);
3512
+ if (envelope.statusCodeValue && envelope.statusCodeValue !== 200) {
3513
+ throw new Error(
3514
+ formatQoderStreamError(envelope.statusCodeValue, envelope.body, Date.now(), providerMode)
3515
+ );
3516
+ }
3517
+ const innerStr = envelope.body;
3518
+ if (innerStr === "[DONE]") {
3519
+ sawDone = true;
3520
+ break;
3521
+ }
3522
+ if (!innerStr) continue;
3523
+ const inner = JSON.parse(innerStr);
3524
+ if (inner.id) output.responseId = inner.id;
3525
+ if (inner.model) output.responseModel = inner.model;
3526
+ if (inner.usage) {
3527
+ const u = inner.usage;
3528
+ const promptTokens = u.prompt_tokens ?? 0;
3529
+ const cacheReadTokens = u.prompt_tokens_details?.cached_tokens ?? 0;
3530
+ const cacheWriteTokens = u.prompt_tokens_details?.cache_write_tokens ?? 0;
3531
+ output.usage.input = Math.max(0, promptTokens - cacheReadTokens - cacheWriteTokens);
3532
+ output.usage.output = u.completion_tokens ?? 0;
3533
+ output.usage.totalTokens = u.total_tokens ?? 0;
3534
+ output.usage.cacheRead = cacheReadTokens;
3535
+ output.usage.cacheWrite = cacheWriteTokens;
3536
+ }
3537
+ if (inner.choices && inner.choices.length > 0) {
3538
+ const choice = inner.choices[0];
3539
+ const delta = choice.delta;
3540
+ if (delta) {
3541
+ if (delta.reasoning_content) {
3542
+ rawReasoningTail = (rawReasoningTail + delta.reasoning_content).slice(-512);
3543
+ const reasoningChunk = stripDsmlResidue(stripThinkingTags(delta.reasoning_content));
3544
+ if (reasoningChunk) {
3545
+ if (thinkingBlockIndex === -1) {
3546
+ thinkingBlockIndex = output.content.length;
3547
+ output.content.push({ type: "thinking", thinking: "" });
3548
+ stream.push({ type: "thinking_start", contentIndex: thinkingBlockIndex, partial: output });
3549
+ }
3550
+ const block = output.content[thinkingBlockIndex];
3551
+ block.thinking += reasoningChunk;
3552
+ stream.push({
3553
+ type: "thinking_delta",
3554
+ contentIndex: thinkingBlockIndex,
3555
+ delta: reasoningChunk,
3556
+ partial: output
3557
+ });
2964
3558
  }
2965
- const block = output.content[contentBlockIndex];
2966
- block.text += delta.content;
2967
- stream.push({
2968
- type: "text_delta",
2969
- contentIndex: contentBlockIndex,
2970
- delta: delta.content,
2971
- partial: output
2972
- });
2973
3559
  }
2974
- }
2975
- if (delta.tool_calls && Array.isArray(delta.tool_calls)) {
2976
- for (const tc of delta.tool_calls) {
2977
- const idx = tc.index ?? 0;
2978
- if (!toolCallsState[idx]) {
2979
- toolCallsState[idx] = { arguments: "", id: "", name: "", contentIndex: 0 };
2980
- }
2981
- const state = toolCallsState[idx];
2982
- if (tc.id) state.id = tc.id;
2983
- if (tc.function?.name) state.name = tc.function.name;
2984
- if (state.emittedStart === void 0 && (state.id || state.name)) {
2985
- state.emittedStart = true;
2986
- state.contentIndex = output.content.length;
2987
- output.content.push({
2988
- type: "toolCall",
2989
- id: state.id,
2990
- name: state.name,
2991
- arguments: {}
3560
+ if (delta.content) {
3561
+ if (thinkingBlockIndex !== -1) {
3562
+ const block = output.content[thinkingBlockIndex];
3563
+ stream.push({
3564
+ type: "thinking_end",
3565
+ contentIndex: thinkingBlockIndex,
3566
+ content: block.thinking,
3567
+ partial: output
2992
3568
  });
2993
- stream.push({ type: "toolcall_start", contentIndex: state.contentIndex, partial: output });
3569
+ thinkingBlockIndex = -1;
2994
3570
  }
2995
- if (state.emittedStart) {
2996
- const block = output.content[state.contentIndex];
2997
- block.id = state.id;
2998
- block.name = state.name;
2999
- }
3000
- if (tc.function?.arguments) {
3001
- const argDelta = tc.function.arguments;
3002
- state.arguments += argDelta;
3571
+ if (thinkingParser) {
3572
+ thinkingParser.processChunk(delta.content);
3573
+ } else {
3574
+ if (contentBlockIndex === -1) {
3575
+ contentBlockIndex = output.content.length;
3576
+ output.content.push({ type: "text", text: "" });
3577
+ stream.push({ type: "text_start", contentIndex: contentBlockIndex, partial: output });
3578
+ }
3579
+ const block = output.content[contentBlockIndex];
3580
+ block.text += delta.content;
3003
3581
  stream.push({
3004
- type: "toolcall_delta",
3005
- contentIndex: state.contentIndex,
3006
- delta: argDelta,
3582
+ type: "text_delta",
3583
+ contentIndex: contentBlockIndex,
3584
+ delta: delta.content,
3007
3585
  partial: output
3008
3586
  });
3009
3587
  }
3010
3588
  }
3589
+ if (delta.tool_calls && Array.isArray(delta.tool_calls)) {
3590
+ for (const tc of delta.tool_calls) {
3591
+ const idx = tc.index ?? 0;
3592
+ if (!toolCallsState[idx]) {
3593
+ toolCallsState[idx] = { arguments: "", id: "", name: "", contentIndex: 0 };
3594
+ }
3595
+ const state = toolCallsState[idx];
3596
+ if (tc.id) state.id = tc.id;
3597
+ if (tc.function?.name) state.name = tc.function.name;
3598
+ if (state.emittedStart === void 0 && (state.id || state.name)) {
3599
+ state.emittedStart = true;
3600
+ state.contentIndex = output.content.length;
3601
+ output.content.push({
3602
+ type: "toolCall",
3603
+ id: state.id,
3604
+ name: state.name,
3605
+ arguments: {}
3606
+ });
3607
+ stream.push({ type: "toolcall_start", contentIndex: state.contentIndex, partial: output });
3608
+ }
3609
+ if (state.emittedStart) {
3610
+ const block = output.content[state.contentIndex];
3611
+ block.id = state.id;
3612
+ block.name = state.name;
3613
+ }
3614
+ if (tc.function?.arguments) {
3615
+ const argDelta = tc.function.arguments;
3616
+ state.arguments += argDelta;
3617
+ stream.push({
3618
+ type: "toolcall_delta",
3619
+ contentIndex: state.contentIndex,
3620
+ delta: argDelta,
3621
+ partial: output
3622
+ });
3623
+ }
3624
+ }
3625
+ }
3626
+ }
3627
+ if (choice.finish_reason) {
3628
+ output.stopReason = choice.finish_reason;
3011
3629
  }
3012
3630
  }
3013
- if (choice.finish_reason) {
3014
- output.stopReason = choice.finish_reason;
3015
- }
3016
- }
3017
- } catch (e) {
3018
- if (e instanceof SyntaxError) {
3019
- if (process.env.QODER_DEBUG) {
3020
- console.error("[pi-provider-qoder] skipping malformed SSE line:", dataStr.slice(0, 200));
3631
+ } catch (e) {
3632
+ if (e instanceof SyntaxError) {
3633
+ if (process.env.QODER_DEBUG) {
3634
+ console.error("[pi-provider-qoder] skipping malformed SSE line:", dataStr.slice(0, 200));
3635
+ }
3636
+ continue;
3021
3637
  }
3022
- continue;
3638
+ throw e;
3023
3639
  }
3024
- throw e;
3025
3640
  }
3026
3641
  }
3027
- }
3028
- await reader.cancel().catch(() => {
3029
- });
3030
- if (thinkingParser) {
3031
- thinkingParser.finalize();
3032
- }
3033
- if (thinkingBlockIndex !== -1) {
3034
- const block = output.content[thinkingBlockIndex];
3035
- stream.push({
3036
- type: "thinking_end",
3037
- contentIndex: thinkingBlockIndex,
3038
- content: block.thinking,
3039
- partial: output
3642
+ await reader.cancel().catch(() => {
3040
3643
  });
3041
- }
3042
- for (const state of toolCallsState) {
3043
- if (state?.emittedStart && !state.emittedEnd) {
3044
- state.emittedEnd = true;
3045
- let args = {};
3046
- try {
3047
- args = JSON.parse(state.arguments || "{}");
3048
- } catch {
3049
- }
3050
- const block = output.content[state.contentIndex];
3051
- block.arguments = args;
3644
+ if (thinkingParser) {
3645
+ thinkingParser.finalize();
3646
+ }
3647
+ if (thinkingBlockIndex !== -1) {
3648
+ const block = output.content[thinkingBlockIndex];
3052
3649
  stream.push({
3053
- type: "toolcall_end",
3054
- contentIndex: state.contentIndex,
3055
- toolCall: {
3056
- type: "toolCall",
3057
- id: state.id,
3058
- name: state.name,
3059
- arguments: args
3060
- },
3650
+ type: "thinking_end",
3651
+ contentIndex: thinkingBlockIndex,
3652
+ content: block.thinking,
3061
3653
  partial: output
3062
3654
  });
3063
3655
  }
3064
- }
3065
- if (toolCallsState.some((state) => state?.emittedStart)) {
3066
- output.stopReason = "toolUse";
3067
- }
3068
- if (isDegenerateDsmlTurn(output, rawReasoningTail)) {
3069
- output.stopReason = "error";
3070
- output.errorMessage = "Qoder server error: degenerate model output (unparseable DSML tool-call markup, no tool call executed)";
3071
- stream.push({ type: "error", reason: "error", error: output });
3656
+ for (const state of toolCallsState) {
3657
+ if (state?.emittedStart && !state.emittedEnd) {
3658
+ state.emittedEnd = true;
3659
+ let args = {};
3660
+ try {
3661
+ args = JSON.parse(state.arguments || "{}");
3662
+ } catch {
3663
+ }
3664
+ const block = output.content[state.contentIndex];
3665
+ block.arguments = args;
3666
+ stream.push({
3667
+ type: "toolcall_end",
3668
+ contentIndex: state.contentIndex,
3669
+ toolCall: {
3670
+ type: "toolCall",
3671
+ id: state.id,
3672
+ name: state.name,
3673
+ arguments: args
3674
+ },
3675
+ partial: output
3676
+ });
3677
+ }
3678
+ }
3679
+ if (toolCallsState.some((state) => state?.emittedStart)) {
3680
+ output.stopReason = "toolUse";
3681
+ }
3682
+ if (isDegenerateDsmlTurn(output, rawReasoningTail)) {
3683
+ output.stopReason = "error";
3684
+ output.errorMessage = "Qoder server error: degenerate model output (unparseable DSML tool-call markup, no tool call executed)";
3685
+ stream.push({ type: "error", reason: "error", error: output });
3686
+ stream.end();
3687
+ return;
3688
+ }
3689
+ stream.push({
3690
+ type: "done",
3691
+ reason: output.stopReason,
3692
+ message: output
3693
+ });
3072
3694
  stream.end();
3073
- return;
3695
+ };
3696
+ try {
3697
+ await runAttempt(void 0);
3698
+ } catch (error) {
3699
+ if (!["qoder", "qoder-cn"].includes(model.provider) || !isCredentialExpiredError(error)) {
3700
+ throw error;
3701
+ }
3702
+ const stored = getCachedCredentials("", model.provider);
3703
+ if (!stored) throw error;
3704
+ const attemptedAccess = options?.apiKey;
3705
+ if (stored.access && attemptedAccess && stored.access !== attemptedAccess) {
3706
+ await runAttempt(stored.access);
3707
+ return;
3708
+ }
3709
+ const lastHeal = healCooldowns.get(model.provider) ?? 0;
3710
+ if (Date.now() - lastHeal < HEAL_COOLDOWN_MS) throw error;
3711
+ healCooldowns.set(model.provider, Date.now());
3712
+ let healed;
3713
+ try {
3714
+ healed = await refreshQoderTokenForMode(stored, providerMode, options?.signal);
3715
+ } catch (healError) {
3716
+ if (healError === error) throw error;
3717
+ healCooldowns.delete(model.provider);
3718
+ throw healError;
3719
+ }
3720
+ saveCredentialsToAuthFile(model.provider, healed);
3721
+ await runAttempt(healed.access);
3722
+ } finally {
3723
+ await reader?.cancel().catch(() => {
3724
+ });
3074
3725
  }
3075
- stream.push({
3076
- type: "done",
3077
- reason: output.stopReason,
3078
- message: output
3079
- });
3080
- stream.end();
3081
3726
  } catch (e) {
3082
3727
  output.stopReason = options?.signal?.aborted ? "aborted" : "error";
3083
3728
  output.errorMessage = e instanceof Error ? e.message : String(e);
@@ -3120,7 +3765,7 @@ function createQoderOAuth(mode) {
3120
3765
  return {
3121
3766
  name: region.loginName,
3122
3767
  login: (callbacks) => loginQoderForMode(callbacks, mode),
3123
- refreshToken: (credentials) => refreshQoderTokenForMode(credentials, mode),
3768
+ refreshToken: (credentials, signal) => refreshQoderTokenForMode(credentials, mode, signal),
3124
3769
  getApiKey: (cred) => cred.access,
3125
3770
  // NOTE: no `modifyModels` hook on purpose. OMP (Bun) does a whole-catalog
3126
3771
  // structuredClone before invoking it, and its bundled catalog contains a
@@ -3190,11 +3835,29 @@ async function index_default(pi) {
3190
3835
  }
3191
3836
  function registerCommands(pi) {
3192
3837
  pi.registerCommand("qoder-cn.usage", {
3193
- description: "Show qoder-cn quota: plan + add-on credits, used/limit and reset (append 'json' for the raw payload)",
3838
+ description: "Show Qoder CN quota: plan + add-on credits, used/limit and reset (append 'json' for the raw payload)",
3194
3839
  handler: async (args, ctx) => {
3195
3840
  await runUsageCommand("cn", args, ctx);
3196
3841
  }
3197
3842
  });
3843
+ pi.registerCommand("qoder-cn.claim", {
3844
+ description: "Claim Qoder CN daily 100 free Credits reward (resets daily at 10:00 UTC+8)",
3845
+ handler: async (args, ctx) => {
3846
+ await runClaimCommand("cn", args, ctx);
3847
+ }
3848
+ });
3849
+ pi.registerCommand("qoder.usage", {
3850
+ description: "Show Qoder Global quota: plan + add-on credits and check-in status (append 'json' for raw payload)",
3851
+ handler: async (args, ctx) => {
3852
+ await runUsageCommand("global", args, ctx);
3853
+ }
3854
+ });
3855
+ pi.registerCommand("qoder.claim", {
3856
+ description: "Claim Qoder Global daily 100 free Credits reward (resets daily at 10:00 UTC+8)",
3857
+ handler: async (args, ctx) => {
3858
+ await runClaimCommand("global", args, ctx);
3859
+ }
3860
+ });
3198
3861
  }
3199
3862
  export {
3200
3863
  index_default as default