pi-provider-qoder-cn 0.2.5 → 0.2.7

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 +4 -12
  2. package/dist/index.js +132 -57
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -18,7 +18,7 @@ pi install npm:pi-provider-qoder-cn
18
18
  | Qoder CN provider (`qoder-cn`) | ✅ | ✅ |
19
19
  | Qoder Global provider (`qoder`) | ✅ | ➖ not registered (see below) |
20
20
  | `/qoder-cn.usage` quota command | ❌ | ✅ |
21
- | `/qoder-endpoint` enterprise (VPC) switching | ❌ | ✅ |
21
+ | Enterprise (VPC) selection during `/login qoder-cn` | ❌ | ✅ |
22
22
  | Current CN model catalog (`qwen3.8-max` / `qwen3.8-flash`, …) | ❌ | ✅ |
23
23
  | Stable CN model ids for `enabledModels` | ❌ | ✅ |
24
24
 
@@ -106,18 +106,10 @@ Notes:
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.
108
108
 
109
- ### `/qoder-endpoint`
109
+ ### Enterprise (VPC) endpoint support
110
110
 
111
- Show or set the CN gateway. Enterprises on a private (VPC) deployment use a
112
- per-tenant host instead of the public one.
113
-
114
- ```text
115
- /qoder-endpoint # show the active endpoint
116
- /qoder-endpoint acme # enterprise instance "acme"
117
- /qoder-endpoint acme-gateway.vpc.qoder.com.cn
118
- /qoder-endpoint https://qoder.internal.example.com
119
- /qoder-endpoint default # back to the public gateway
120
- ```
111
+ Enterprises on a private (VPC) deployment can select their endpoint directly
112
+ during `/login qoder-cn` or set `QODER_VPC_ENDPOINT`.
121
113
 
122
114
  A bare instance label `acme` expands to the standard enterprise hostnames:
123
115
 
package/dist/index.js CHANGED
@@ -1754,51 +1754,121 @@ async function fetchQoderUsageForMode(credentials, mode) {
1754
1754
  // src/index.ts
1755
1755
  init_catalog();
1756
1756
 
1757
- // src/commands/endpoint.ts
1758
- init_oauth();
1759
- init_catalog();
1760
- init_vpc();
1761
- var RESET_VALUES = /* @__PURE__ */ new Set(["official", "default", "clear", "none", "reset"]);
1762
- function describeEndpoint() {
1763
- const current = getQoderCNEndpoints();
1764
- if (current.isDefault) {
1765
- return "Official Default Gateway (https://gateway.qoder.com.cn/)";
1766
- }
1767
- return `Enterprise Endpoint [${current.raw}]: ${current.baseUrl}`;
1768
- }
1769
- async function runEndpointCommand(args, ctx) {
1770
- const input = (args || "").trim();
1771
- if (!input) {
1772
- ctx?.ui?.notify(
1773
- `Current Qoder CN endpoint: ${describeEndpoint()}
1774
- To set: /qoder-endpoint <domain>
1775
- To reset: /qoder-endpoint default`,
1776
- "info"
1777
- );
1778
- return;
1757
+ // src/protocol/errors.ts
1758
+ function parseQoderErrorPayload(payload) {
1759
+ if (!payload) return null;
1760
+ let parsed = payload;
1761
+ if (typeof payload === "string") {
1762
+ try {
1763
+ parsed = JSON.parse(payload);
1764
+ } catch {
1765
+ return null;
1766
+ }
1779
1767
  }
1780
- try {
1781
- const isReset = RESET_VALUES.has(input.toLowerCase());
1782
- const resolved = setQoderCNEndpoint(isReset ? "" : input);
1783
- ctx?.ui?.notify(
1784
- isReset ? "Qoder CN endpoint reset to Official Default Gateway (https://gateway.qoder.com.cn/)" : `Qoder CN endpoint set to: ${resolved.baseUrl}`,
1785
- "info"
1786
- );
1787
- const credentials = getCachedCredentials("", "qoder-cn");
1788
- if (credentials?.access) {
1789
- await updateQoderModelsCache(
1790
- credentials.access,
1791
- credentials.userID,
1792
- credentials.name,
1793
- credentials.email,
1794
- "cn"
1795
- ).catch(() => void 0);
1796
- ctx?.ui?.notify("Qoder CN model catalog refreshed.", "info");
1768
+ if (typeof parsed !== "object" || parsed === null) return null;
1769
+ const obj = parsed;
1770
+ if (typeof obj.message === "string" && obj.message.trim().startsWith("{")) {
1771
+ try {
1772
+ const nested = JSON.parse(obj.message);
1773
+ if (typeof nested === "object" && nested !== null) {
1774
+ const nestedObj = nested;
1775
+ return {
1776
+ code: String(nestedObj.code || nestedObj.errorCode || obj.code || ""),
1777
+ message: String(nestedObj.message || nestedObj.errorMessage || obj.message || "")
1778
+ };
1779
+ }
1780
+ } catch {
1797
1781
  }
1798
- } catch (error) {
1799
- const message = error instanceof Error ? error.message : String(error);
1800
- ctx?.ui?.notify(`Failed to set Qoder CN endpoint: ${message}`, "error");
1801
1782
  }
1783
+ const code = obj.code ?? obj.errorCode;
1784
+ const message = obj.message ?? obj.errorMessage;
1785
+ if (!code && !message) return null;
1786
+ return {
1787
+ code: code !== void 0 ? String(code) : void 0,
1788
+ message: message !== void 0 ? String(message) : void 0
1789
+ };
1790
+ }
1791
+ function msUntilBeijingMidnight(now = Date.now()) {
1792
+ const beijingOffset = 8 * 36e5;
1793
+ const beijingTime = now + beijingOffset;
1794
+ const beijingDate = new Date(beijingTime);
1795
+ const nextMidnightUtc = Date.UTC(
1796
+ beijingDate.getUTCFullYear(),
1797
+ beijingDate.getUTCMonth(),
1798
+ beijingDate.getUTCDate() + 1,
1799
+ 0,
1800
+ 0,
1801
+ 0
1802
+ );
1803
+ return nextMidnightUtc - beijingTime;
1804
+ }
1805
+ function getBeijingDailyResetCountdown(now = Date.now()) {
1806
+ const diffMs = msUntilBeijingMidnight(now);
1807
+ const totalMinutes = Math.max(1, Math.round(diffMs / 6e4));
1808
+ const hours = Math.floor(totalMinutes / 60);
1809
+ const minutes = totalMinutes % 60;
1810
+ return hours > 0 ? `${hours}\u5C0F\u65F6${minutes}\u5206\u949F` : `${minutes}\u5206\u949F`;
1811
+ }
1812
+ function formatQoderStreamError(statusCode, rawBody, now = Date.now()) {
1813
+ const parsed = parseQoderErrorPayload(rawBody);
1814
+ const code = parsed?.code;
1815
+ const message = parsed?.message || (typeof rawBody === "string" ? rawBody : JSON.stringify(rawBody));
1816
+ const countdown = getBeijingDailyResetCountdown(now);
1817
+ if (code === "110" || /billing daily count exceeded/i.test(message) || /daily usage limit reached/i.test(message)) {
1818
+ return [
1819
+ `[Qoder CN \u989D\u5EA6\u9650\u5236] \u4ECA\u65E5\u8C03\u7528\u6B21\u6570\u5DF2\u8FBE\u4E0A\u9650 (Billing daily count exceeded, \u9519\u8BEF\u7801 110)`,
1820
+ `- \u5237\u65B0\u65F6\u95F4\uFF1A\u5C06\u5728\u5317\u4EAC\u65F6\u95F4 00:00 \u91CD\u7F6E\uFF08\u7EA6 ${countdown}\u540E\uFF09`,
1821
+ `- \u9650\u5236\u539F\u56E0\uFF1A\u5F53\u524D\u5904\u4E8E\u4E2A\u4EBA\u6807\u51C6\u7248\uFF08\u514D\u8D39\u7248\uFF09\u6216\u89E6\u53D1\u4E86\u5355\u65E5\u9891\u6B21\u7194\u65AD\u3002\u82E5\u8D26\u53F7\u5185\u6709\u8D44\u6E90\u5305/\u52A0\u6CB9\u5305\uFF08Add-on Credits\uFF09\uFF0C\u56E0\u514D\u8D39\u7248\u5355\u65E5\u9650\u5236\u672A\u89E3\u9664\u6682\u65E0\u6CD5\u6263\u51CF\u3002`,
1822
+ `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u7B49\u5F85\u6B21\u65E5\u91CD\u7F6E\uFF1B\u6216\u4F7F\u7528 /model \u5207\u6362\u81F3\u5176\u4ED6 Provider\uFF1B\u4E2A\u4EBA\u7248\u53EF\u5F00\u901A Pro \u8BA2\u9605\u89E3\u9664\u6BCF\u65E5\u9650\u5236\uFF0C\u4F01\u4E1A\u7248\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u8C03\u9AD8\u5355\u65E5\u9650\u989D\u3002`
1823
+ ].join("\n");
1824
+ }
1825
+ if (code === "117" || /team member credits exhausted/i.test(message)) {
1826
+ return [
1827
+ `[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)`,
1828
+ `- \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`,
1829
+ `- \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`
1830
+ ].join("\n");
1831
+ }
1832
+ if (code === "116" || /team administrator credits exhausted/i.test(message)) {
1833
+ return [
1834
+ `[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)`,
1835
+ `- \u9650\u5236\u539F\u56E0\uFF1A\u5F53\u524D\u4F01\u4E1A\u7EC4\u7EC7\u8D26\u6237\u7684 Credits \u989D\u5EA6\u5DF2\u5168\u90E8\u7528\u5B8C\u3002`,
1836
+ `- \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`
1837
+ ].join("\n");
1838
+ }
1839
+ if (code === "122" || /billing-group credits limit reached/i.test(message)) {
1840
+ return [
1841
+ `[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)`,
1842
+ `- \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`,
1843
+ `- \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`
1844
+ ].join("\n");
1845
+ }
1846
+ if (code === "119" || /free usage limit for the selected model reached/i.test(message)) {
1847
+ return [
1848
+ `[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)`,
1849
+ `- \u5237\u65B0\u65F6\u95F4\uFF1A\u5C06\u5728\u5317\u4EAC\u65F6\u95F4 00:00 \u91CD\u7F6E\uFF08\u7EA6 ${countdown}\u540E\uFF09`,
1850
+ `- \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`
1851
+ ].join("\n");
1852
+ }
1853
+ if (code === "113" || code === "118" || /quota exhausted/i.test(message) || /credits exhausted/i.test(message)) {
1854
+ return [
1855
+ `[Qoder CN \u989D\u5EA6\u8017\u5C3D] \u8D26\u6237 Credits \u4F59\u989D\u5DF2\u5168\u90E8\u7528\u5C3D (\u9519\u8BEF\u7801 ${code || 113})`,
1856
+ `- \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`,
1857
+ `- \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`
1858
+ ].join("\n");
1859
+ }
1860
+ if (code === "105" || /token expired/i.test(message) || /login expired/i.test(message)) {
1861
+ return [
1862
+ `[Qoder CN \u51ED\u8BC1\u5931\u6548] \u767B\u5F55\u6001\u5DF2\u8FC7\u671F\u6216 Token \u5931\u6548 (\u9519\u8BEF\u7801 105)`,
1863
+ `- \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`
1864
+ ].join("\n");
1865
+ }
1866
+ if (rawBody === void 0 || rawBody === null) {
1867
+ return `Upstream status ${statusCode}`;
1868
+ }
1869
+ const bodyStr = typeof rawBody === "string" ? rawBody : JSON.stringify(rawBody);
1870
+ const truncated = bodyStr.length > 500 ? `${bodyStr.slice(0, 500)}...` : bodyStr;
1871
+ return `Upstream status ${statusCode}: ${truncated}`;
1802
1872
  }
1803
1873
 
1804
1874
  // src/commands/usage.ts
@@ -1844,6 +1914,14 @@ function formatPercent(value) {
1844
1914
  if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
1845
1915
  return Math.round(value * 10) / 10;
1846
1916
  }
1917
+ function formatDailyResetCountdown(now = Date.now()) {
1918
+ const diffMs = msUntilBeijingMidnight(now);
1919
+ const totalMinutes = Math.max(1, Math.round(diffMs / 6e4));
1920
+ const hours = Math.floor(totalMinutes / 60);
1921
+ const minutes = totalMinutes % 60;
1922
+ const countdown = hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
1923
+ return `00:00 UTC+8 (in ${countdown})`;
1924
+ }
1847
1925
  function formatResetTime(expiresAt, now = Date.now()) {
1848
1926
  const ms = Number(expiresAt);
1849
1927
  if (!Number.isFinite(ms) || ms <= 0) return "n/a";
@@ -1932,10 +2010,16 @@ function formatQoderUsage(raw, mode, now = Date.now(), options = {}) {
1932
2010
  if (showAddOnQuota) rows.push(bucketRow("Add-on", pickBucket(raw, "addOnQuota"), danger));
1933
2011
  if (showOrg && org)
1934
2012
  rows.push(bucketRow("Enterprise", { ...org, percentage: void 0, total: org.cap ?? org.total }, danger));
2013
+ const isPersonalStandard = raw?.userType === "personal_standard" || raw?.userType === "free";
1935
2014
  const notes = [];
1936
- if (danger) notes.push({ label: "Status", value: "quota exceeded", color: ANSI.red });
2015
+ if (danger) {
2016
+ notes.push({ label: "Status", value: "quota exceeded", color: ANSI.red });
2017
+ }
1937
2018
  if (!showUserQuota && !showAddOnQuota && !showOrg) notes.push({ label: "Buckets", value: "none returned" });
1938
2019
  if (raw?.isPlanQuotaProrated) notes.push({ label: "Note", value: "plan quota is prorated" });
2020
+ if (isPersonalStandard || !showOrg && formatResetTime(raw?.expiresAt, now) === "never") {
2021
+ notes.push({ label: "Daily reset", value: formatDailyResetCountdown(now) });
2022
+ }
1939
2023
  notes.push({ label: "Expires", value: formatResetTime(raw?.expiresAt, now) });
1940
2024
  const manageUrl = raw?.addOnQuota?.detailUrl || raw?.upgradeUrl || region.manageUrl;
1941
2025
  if (manageUrl) notes.push({ label: "Manage", value: manageUrl });
@@ -2729,7 +2813,7 @@ function streamQoder(model, context, options) {
2729
2813
  });
2730
2814
  if (!response.ok) {
2731
2815
  const errText = await response.text();
2732
- throw new Error(`Qoder API request failed: ${response.status} ${response.statusText}. Response: ${errText}`);
2816
+ throw new Error(formatQoderStreamError(response.status, errText));
2733
2817
  }
2734
2818
  const body = response.body;
2735
2819
  if (!body) throw new Error("No response body");
@@ -2783,7 +2867,7 @@ function streamQoder(model, context, options) {
2783
2867
  try {
2784
2868
  const envelope = JSON.parse(dataStr);
2785
2869
  if (envelope.statusCodeValue && envelope.statusCodeValue !== 200) {
2786
- throw new Error(`Upstream status ${envelope.statusCodeValue}: ${envelope.body}`);
2870
+ throw new Error(formatQoderStreamError(envelope.statusCodeValue, envelope.body));
2787
2871
  }
2788
2872
  const innerStr = envelope.body;
2789
2873
  if (innerStr === "[DONE]") {
@@ -3074,19 +3158,10 @@ async function index_default(pi) {
3074
3158
  registerCommands(pi);
3075
3159
  }
3076
3160
  function registerCommands(pi) {
3077
- for (const mode of QODER_PROVIDER_MODES) {
3078
- const providerID = getQoderRegionConfig(mode).providerID;
3079
- pi.registerCommand(`${providerID}.usage`, {
3080
- description: `Show ${providerID} quota: plan + add-on credits, used/limit and reset (append 'json' for the raw payload)`,
3081
- handler: async (args, ctx) => {
3082
- await runUsageCommand(mode, args, ctx);
3083
- }
3084
- });
3085
- }
3086
- pi.registerCommand("qoder-endpoint", {
3087
- description: "Show or set the Qoder CN gateway endpoint (use 'default' to reset)",
3161
+ pi.registerCommand("qoder-cn.usage", {
3162
+ description: "Show qoder-cn quota: plan + add-on credits, used/limit and reset (append 'json' for the raw payload)",
3088
3163
  handler: async (args, ctx) => {
3089
- await runEndpointCommand(args, ctx);
3164
+ await runUsageCommand("cn", args, ctx);
3090
3165
  }
3091
3166
  });
3092
3167
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-provider-qoder-cn",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
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",