pi-provider-qoder-cn 0.2.24 → 0.2.26

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 +38 -3
  2. package/dist/index.js +600 -667
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -15,16 +15,16 @@ var __export = (target, all) => {
15
15
 
16
16
  // src/cn-naming.ts
17
17
  function slugifyQoderCNModel(displayName) {
18
- const slug = (displayName || "").trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9._-]/g, "").replace(/^-+|-+$/g, "");
18
+ const slug = (displayName || "").trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9._-]/g, "").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-");
19
19
  return slug || "qodermodel";
20
20
  }
21
21
  function prettifyQoderCNModelName(displayName) {
22
- const pretty = (displayName || "Qoder CN Model").replace(/Qwen(\d)/g, "Qwen $1").replace(/Qwen([\d.]+)-/g, "Qwen $1 ").replace(/DeepSeek\s*V(\d)-/g, "DeepSeek V$1 ").replace(/\s+/g, " ").trim();
22
+ const pretty = (displayName || "Qoder CN Model").replace(/Qwen(\d)/g, "Qwen $1").replace(/Qwen([\d.]+)-/g, "Qwen $1 ").replace(/DeepSeek([Vv]\d)-/g, "DeepSeek $1 ").replace(/\s+/g, " ").trim();
23
23
  return pretty.includes("Qoder CN") ? pretty : `${pretty} \xB7 Qoder CN`;
24
24
  }
25
25
  function toQoderCNModelInfo(upstreamKey, displayName) {
26
- const friendly = upstreamKey ? QODER_CN_FRIENDLY_MODELS[upstreamKey] : void 0;
27
- if (friendly) return friendly;
26
+ const fixed = upstreamKey ? FIXED_CN_MODEL_IDS[upstreamKey] : void 0;
27
+ if (fixed) return fixed;
28
28
  return {
29
29
  id: slugifyQoderCNModel(displayName || upstreamKey),
30
30
  name: prettifyQoderCNModelName(displayName || upstreamKey)
@@ -32,10 +32,9 @@ function toQoderCNModelInfo(upstreamKey, displayName) {
32
32
  }
33
33
  function toQoderCNUpstreamKey(modelId) {
34
34
  const id = modelId || "";
35
- const alias = CN_MODEL_KEY_ALIASES[id];
36
- if (alias) return alias;
37
- if (QODER_CN_FRIENDLY_MODELS[id]) return id;
38
- if (Object.hasOwn(QODER_CN_FRIENDLY_MODELS, id)) return id;
35
+ const legacy = LEGACY_CN_MODEL_IDS[id];
36
+ if (legacy) return legacy;
37
+ if (FIXED_CN_MODEL_IDS[id]) return id;
39
38
  return id || "auto";
40
39
  }
41
40
  function isQoderCNModelId(entry, modelId) {
@@ -43,52 +42,33 @@ function isQoderCNModelId(entry, modelId) {
43
42
  const info = toQoderCNModelInfo(entry.key, entry.display_name);
44
43
  if (info.id === modelId) return true;
45
44
  if (entry.key === modelId) return true;
45
+ const legacyKey = LEGACY_CN_MODEL_IDS[modelId];
46
+ if (legacyKey !== void 0 && legacyKey === entry.key) return true;
46
47
  const stripped = (entry.display_name || "").replace(/\s+/g, "");
47
48
  return stripped.toLowerCase() === modelId.toLowerCase();
48
49
  }
49
- var QODER_CN_FRIENDLY_MODELS, CN_MODEL_KEY_ALIASES;
50
+ var LEGACY_CN_MODEL_IDS, FIXED_CN_MODEL_IDS;
50
51
  var init_cn_naming = __esm({
51
52
  "src/cn-naming.ts"() {
52
53
  "use strict";
53
- QODER_CN_FRIENDLY_MODELS = Object.freeze({
54
- auto: { id: "auto", name: "Auto \xB7 Qoder CN" },
55
- "qoder-cn": { id: "auto", name: "Auto \xB7 Qoder CN" },
56
- qmodel_38max: { id: "qwen3.8-max", name: "Qwen 3.8-Max \xB7 Qoder CN" },
57
- qfmodel: { id: "qwen3.8-flash", name: "Qwen 3.8-Flash \xB7 Qoder CN" },
58
- qmodel_latest: { id: "qwen3.7-max", name: "Qwen 3.7-Max \xB7 Qoder CN" },
59
- qmodel: { id: "qwen3.7-plus", name: "Qwen 3.7-Plus \xB7 Qoder CN" },
60
- q37fmodel: { id: "qwen3.7-flash", name: "Qwen 3.7-Flash \xB7 Qoder CN" },
61
- dmodel: { id: "deepseek-v4-pro", name: "DeepSeek V4 Pro \xB7 Qoder CN" },
62
- dfmodel: { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash \xB7 Qoder CN" },
63
- gmodel: { id: "glm-5.3", name: "GLM-5.3 \xB7 Qoder CN" },
64
- gfmodel: { id: "glm-5.3-flash", name: "GLM-5.3-Flash \xB7 Qoder CN" },
65
- gm51model: { id: "glm-5.2", name: "GLM 5.2 \xB7 Qoder CN" },
66
- kmodel_latest: { id: "kimi-k3", name: "Kimi-K3 \xB7 Qoder CN" },
67
- kmodel: { id: "kimi-k2.7-code", name: "Kimi-K2.7-Code \xB7 Qoder CN" },
68
- mmodel: { id: "minimax-m2.7", name: "MiniMax M2.7 \xB7 Qoder CN" }
69
- });
70
- CN_MODEL_KEY_ALIASES = Object.freeze({
71
- "qwen3.8-max": "qmodel_38max",
72
- "qwen3.8-flash": "qfmodel",
73
- "qwen3.7-max": "qmodel_latest",
74
- "qwen3.7-plus": "qmodel",
54
+ LEGACY_CN_MODEL_IDS = Object.freeze({
55
+ "qwen3.6-flash": "qfmodel",
56
+ "qwen3.6-plus": "qmodel",
75
57
  "qwen3.7-flash": "q37fmodel",
76
- "deepseek-v4-pro": "dmodel",
77
- "deepseek-v4-flash": "dfmodel",
78
- "glm-5.3": "gmodel",
79
- "glm-5.3-flash": "gfmodel",
80
- "glm-5.2": "gm51model",
58
+ "qwen3.7-max": "qmodel_latest",
81
59
  "glm-5.1": "gm51model",
82
- "kimi-k3": "kmodel_latest",
83
- "kimi-k2.7-code": "kmodel",
84
- "kimi-k2.7": "kmodel",
60
+ "glm-5.2": "gm51model",
85
61
  "kimi-k2.6": "kmodel",
62
+ "kimi-k2.7": "kmodel",
63
+ "kimi-k2.7-code": "kmodel",
86
64
  "kimi-k2.8-preview": "kmodel",
87
- "qwen3.6-flash": "qfmodel",
88
- "qwen3.6-plus": "qmodel",
65
+ "kimi-k3": "kmodel_latest",
89
66
  "minimax-m2.7": "mmodel",
90
67
  "minimax-m3": "mmodel"
91
68
  });
69
+ FIXED_CN_MODEL_IDS = Object.freeze({
70
+ auto: { id: "auto", name: "Auto \xB7 Qoder CN" }
71
+ });
92
72
  }
93
73
  });
94
74
 
@@ -453,6 +433,9 @@ function getHomeDir3() {
453
433
  function getQoderCachePath(mode) {
454
434
  return join3(getHomeDir3(), ".pi", "agent", getQoderRegionConfig(mode).modelCacheFile);
455
435
  }
436
+ function clearQoderModelsMemCache() {
437
+ modelCacheMem.clear();
438
+ }
456
439
  function readParsedModelCache(mode) {
457
440
  const cachePath = getQoderCachePath(mode);
458
441
  if (modelCacheMem.has(cachePath)) {
@@ -629,11 +612,11 @@ async function updateQoderModelsCache(authToken, userID, name, email, mode) {
629
612
  }
630
613
  });
631
614
  if (!response.ok) {
632
- return;
615
+ return null;
633
616
  }
634
617
  const resData = await response.json();
635
618
  const chatModels = resData.chat || [];
636
- if (chatModels.length === 0) return;
619
+ if (chatModels.length === 0) return null;
637
620
  const newModels = [];
638
621
  const configs = {};
639
622
  for (const entry of chatModels) {
@@ -664,14 +647,28 @@ async function updateQoderModelsCache(authToken, userID, name, email, mode) {
664
647
  maxTokens: MAX_OUTPUT_TOKENS
665
648
  });
666
649
  }
667
- if (newModels.length === 0) return;
650
+ if (newModels.length === 0) return null;
651
+ const previous = readParsedModelCache(mode);
652
+ const previousModels = new Map((previous?.models ?? []).map((model) => [model.id, model.name]));
653
+ const nextIds = new Set(newModels.map((model) => model.id));
654
+ const diff = {
655
+ added: newModels.filter((model) => !previousModels.has(model.id)).map((model) => ({ id: model.id, name: model.name })),
656
+ renamed: newModels.filter((model) => {
657
+ const before = previousModels.get(model.id);
658
+ return before !== void 0 && before !== model.name;
659
+ }).map((model) => ({ id: model.id, from: previousModels.get(model.id) ?? "", to: model.name })),
660
+ removed: [...previousModels].filter(([id]) => !nextIds.has(id)).map(([id, previousName]) => ({ id, name: previousName })),
661
+ total: newModels.length
662
+ };
668
663
  const cacheData = {
669
664
  updatedAt: Date.now(),
670
665
  models: newModels,
671
666
  configs
672
667
  };
673
668
  writeParsedModelCache(mode, cacheData);
669
+ return diff;
674
670
  } catch {
671
+ return null;
675
672
  }
676
673
  }
677
674
  var ZERO_COST, MAX_OUTPUT_TOKENS, DEFAULT_CONTEXT_WINDOW, QODER_CACHE_WRITER, modelCacheMem, staticModels, staticCnModels, PI_THINKING_LEVELS;
@@ -915,203 +912,6 @@ var init_catalog = __esm({
915
912
  contextWindow: 2e5,
916
913
  maxTokens: MAX_OUTPUT_TOKENS,
917
914
  description: "Qoder CN smart routing; live catalog reports 200K max input."
918
- },
919
- {
920
- id: "qwen3.8-max",
921
- upstreamKey: "qmodel_38max",
922
- name: "Qwen 3.8-Max \xB7 Qoder CN",
923
- api: "qoder-api",
924
- provider: "qoder-cn",
925
- baseUrl: getQoderBaseUrl("cn"),
926
- reasoning: true,
927
- supportsEffort: true,
928
- input: ["text", "image"],
929
- cost: ZERO_COST,
930
- contextWindow: DEFAULT_CONTEXT_WINDOW,
931
- maxTokens: MAX_OUTPUT_TOKENS,
932
- description: "Qwen 3.8-Max (qmodel_38max); 1M context."
933
- },
934
- {
935
- id: "qwen3.8-flash",
936
- upstreamKey: "qfmodel",
937
- name: "Qwen 3.8-Flash \xB7 Qoder CN",
938
- api: "qoder-api",
939
- provider: "qoder-cn",
940
- baseUrl: getQoderBaseUrl("cn"),
941
- reasoning: true,
942
- supportsEffort: true,
943
- input: ["text", "image"],
944
- cost: ZERO_COST,
945
- contextWindow: DEFAULT_CONTEXT_WINDOW,
946
- maxTokens: MAX_OUTPUT_TOKENS,
947
- description: "Qwen 3.8-Flash (qfmodel); multimodal MoE, 1M context."
948
- },
949
- {
950
- id: "qwen3.7-max",
951
- upstreamKey: "qmodel_latest",
952
- name: "Qwen 3.7-Max \xB7 Qoder CN",
953
- api: "qoder-api",
954
- provider: "qoder-cn",
955
- baseUrl: getQoderBaseUrl("cn"),
956
- reasoning: true,
957
- supportsEffort: false,
958
- input: ["text", "image"],
959
- cost: ZERO_COST,
960
- contextWindow: DEFAULT_CONTEXT_WINDOW,
961
- maxTokens: MAX_OUTPUT_TOKENS,
962
- description: "Qwen 3.7-Max (qmodel_latest); context options 200K/400K/1M."
963
- },
964
- {
965
- id: "qwen3.7-plus",
966
- upstreamKey: "qmodel",
967
- name: "Qwen 3.7-Plus \xB7 Qoder CN",
968
- api: "qoder-api",
969
- provider: "qoder-cn",
970
- baseUrl: getQoderBaseUrl("cn"),
971
- reasoning: true,
972
- supportsEffort: false,
973
- input: ["text", "image"],
974
- cost: ZERO_COST,
975
- contextWindow: DEFAULT_CONTEXT_WINDOW,
976
- maxTokens: MAX_OUTPUT_TOKENS,
977
- description: "Qwen 3.7-Plus (qmodel); context options 200K/400K/1M."
978
- },
979
- {
980
- id: "qwen3.7-flash",
981
- upstreamKey: "q37fmodel",
982
- name: "Qwen 3.7-Flash \xB7 Qoder CN",
983
- api: "qoder-api",
984
- provider: "qoder-cn",
985
- baseUrl: getQoderBaseUrl("cn"),
986
- reasoning: true,
987
- supportsEffort: false,
988
- input: ["text", "image"],
989
- cost: ZERO_COST,
990
- contextWindow: DEFAULT_CONTEXT_WINDOW,
991
- maxTokens: MAX_OUTPUT_TOKENS,
992
- description: "Qwen 3.7-Flash (q37fmodel); vision-language Flash, 1M context."
993
- },
994
- {
995
- id: "deepseek-v4-pro",
996
- upstreamKey: "dmodel",
997
- name: "DeepSeek V4 Pro \xB7 Qoder CN",
998
- api: "qoder-api",
999
- provider: "qoder-cn",
1000
- baseUrl: getQoderBaseUrl("cn"),
1001
- reasoning: true,
1002
- supportsEffort: true,
1003
- input: ["text", "image"],
1004
- cost: ZERO_COST,
1005
- contextWindow: DEFAULT_CONTEXT_WINDOW,
1006
- maxTokens: MAX_OUTPUT_TOKENS,
1007
- description: "DeepSeek V4 Pro (dmodel); 1M context."
1008
- },
1009
- {
1010
- id: "deepseek-v4-flash",
1011
- upstreamKey: "dfmodel",
1012
- name: "DeepSeek V4 Flash \xB7 Qoder CN",
1013
- api: "qoder-api",
1014
- provider: "qoder-cn",
1015
- baseUrl: getQoderBaseUrl("cn"),
1016
- reasoning: true,
1017
- supportsEffort: true,
1018
- input: ["text", "image"],
1019
- cost: ZERO_COST,
1020
- contextWindow: DEFAULT_CONTEXT_WINDOW,
1021
- maxTokens: MAX_OUTPUT_TOKENS,
1022
- description: "DeepSeek V4 Flash (dfmodel); 1M context."
1023
- },
1024
- {
1025
- id: "glm-5.3",
1026
- upstreamKey: "gmodel",
1027
- name: "GLM-5.3 \xB7 Qoder CN",
1028
- api: "qoder-api",
1029
- provider: "qoder-cn",
1030
- baseUrl: getQoderBaseUrl("cn"),
1031
- reasoning: true,
1032
- supportsEffort: true,
1033
- input: ["text", "image"],
1034
- cost: ZERO_COST,
1035
- contextWindow: DEFAULT_CONTEXT_WINDOW,
1036
- maxTokens: MAX_OUTPUT_TOKENS,
1037
- description: "GLM-5.3 (gmodel); Zhipu flagship open-source, 1M context."
1038
- },
1039
- {
1040
- id: "glm-5.3-flash",
1041
- upstreamKey: "gfmodel",
1042
- name: "GLM-5.3-Flash \xB7 Qoder CN",
1043
- api: "qoder-api",
1044
- provider: "qoder-cn",
1045
- baseUrl: getQoderBaseUrl("cn"),
1046
- reasoning: true,
1047
- supportsEffort: true,
1048
- input: ["text", "image"],
1049
- cost: ZERO_COST,
1050
- contextWindow: DEFAULT_CONTEXT_WINDOW,
1051
- maxTokens: MAX_OUTPUT_TOKENS,
1052
- description: "GLM-5.3-Flash (gfmodel); 1M context."
1053
- },
1054
- {
1055
- id: "glm-5.2",
1056
- upstreamKey: "gm51model",
1057
- name: "GLM 5.2 \xB7 Qoder CN",
1058
- api: "qoder-api",
1059
- provider: "qoder-cn",
1060
- baseUrl: getQoderBaseUrl("cn"),
1061
- reasoning: true,
1062
- supportsEffort: true,
1063
- input: ["text", "image"],
1064
- cost: ZERO_COST,
1065
- contextWindow: DEFAULT_CONTEXT_WINDOW,
1066
- maxTokens: MAX_OUTPUT_TOKENS,
1067
- description: "GLM 5.2 (gm51model); 1M context."
1068
- },
1069
- {
1070
- id: "kimi-k3",
1071
- upstreamKey: "kmodel_latest",
1072
- name: "Kimi-K3 \xB7 Qoder CN",
1073
- api: "qoder-api",
1074
- provider: "qoder-cn",
1075
- baseUrl: getQoderBaseUrl("cn"),
1076
- reasoning: true,
1077
- supportsEffort: true,
1078
- input: ["text", "image"],
1079
- cost: ZERO_COST,
1080
- contextWindow: DEFAULT_CONTEXT_WINDOW,
1081
- maxTokens: MAX_OUTPUT_TOKENS,
1082
- description: "Kimi-K3 (kmodel_latest); 1M context."
1083
- },
1084
- {
1085
- id: "kimi-k2.7-code",
1086
- upstreamKey: "kmodel",
1087
- name: "Kimi-K2.7-Code \xB7 Qoder CN",
1088
- api: "qoder-api",
1089
- provider: "qoder-cn",
1090
- baseUrl: getQoderBaseUrl("cn"),
1091
- reasoning: true,
1092
- supportsEffort: false,
1093
- input: ["text", "image"],
1094
- cost: ZERO_COST,
1095
- // Live catalog advertises 256K for this entry.
1096
- contextWindow: 256e3,
1097
- maxTokens: MAX_OUTPUT_TOKENS,
1098
- description: "Kimi-K2.7-Code (kmodel); 256K context."
1099
- },
1100
- {
1101
- id: "minimax-m2.7",
1102
- upstreamKey: "mmodel",
1103
- name: "MiniMax M2.7 \xB7 Qoder CN",
1104
- api: "qoder-api",
1105
- provider: "qoder-cn",
1106
- baseUrl: getQoderBaseUrl("cn"),
1107
- reasoning: false,
1108
- supportsEffort: false,
1109
- input: ["text"],
1110
- cost: ZERO_COST,
1111
- // Live CN catalog reports 200K; not confirmed at 1M.
1112
- contextWindow: 2e5,
1113
- maxTokens: MAX_OUTPUT_TOKENS,
1114
- description: "MiniMax M2.7 (mmodel); 200K context."
1115
915
  }
1116
916
  ];
1117
917
  PI_THINKING_LEVELS = ["minimal", "low", "medium", "high", "xhigh", "max"];
@@ -1791,35 +1591,415 @@ var init_oauth = __esm({
1791
1591
  // src/index.ts
1792
1592
  init_oauth();
1793
1593
 
1794
- // src/quota.ts
1795
- function quotaNumber(value) {
1796
- return typeof value === "number" && Number.isFinite(value) ? value : 0;
1797
- }
1798
- function bucketLimit(bucket) {
1799
- return quotaNumber(bucket.total ?? bucket.cap);
1594
+ // src/protocol/errors.ts
1595
+ init_region();
1596
+ init_vpc();
1597
+ import { createHash } from "node:crypto";
1598
+
1599
+ // src/protocol/queue.ts
1600
+ var MAX_WAIT_MS = 12e4;
1601
+ var DEFAULT_RETRY_SECONDS = 30;
1602
+ function digForQueue(node, depth) {
1603
+ if (depth > 6) return null;
1604
+ if (typeof node === "string") {
1605
+ const trimmed = node.trim();
1606
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return null;
1607
+ try {
1608
+ return digForQueue(JSON.parse(trimmed), depth + 1);
1609
+ } catch {
1610
+ return null;
1611
+ }
1612
+ }
1613
+ if (node === null || typeof node !== "object") return null;
1614
+ if (Array.isArray(node)) {
1615
+ for (const item of node) {
1616
+ const found = digForQueue(item, depth + 1);
1617
+ if (found) return found;
1618
+ }
1619
+ return null;
1620
+ }
1621
+ const record = node;
1622
+ if (record.isQueued === true) return record;
1623
+ for (const value of Object.values(record)) {
1624
+ const found = digForQueue(value, depth + 1);
1625
+ if (found) return found;
1626
+ }
1627
+ return null;
1800
1628
  }
1801
- function hasAllowance(bucket) {
1802
- if (!bucket) return false;
1803
- return bucketLimit(bucket) > 0 || quotaNumber(bucket.used) > 0 || quotaNumber(bucket.remaining) > 0;
1629
+ function positiveNumber(value, fallback) {
1630
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
1804
1631
  }
1805
- function pickBucket(raw, camel) {
1806
- if (!raw) return void 0;
1807
- const source = raw;
1808
- const snake = camel.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
1809
- return source[snake] ?? source[camel];
1632
+ function textOf(value) {
1633
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
1810
1634
  }
1811
-
1812
- // src/auth/usage.ts
1813
- init_region();
1814
- function round2(value) {
1815
- return Math.round(value * 100) / 100;
1635
+ function parseQueueNotice(envelope) {
1636
+ const payload = digForQueue(envelope, 0);
1637
+ if (!payload) return null;
1638
+ const retryMs = positiveNumber(payload.retry_after_ms, 0) || positiveNumber(payload.retryAfterSeconds, DEFAULT_RETRY_SECONDS) * 1e3;
1639
+ return {
1640
+ retryAfterMs: Math.min(retryMs, MAX_WAIT_MS),
1641
+ waitTimeSeconds: positiveNumber(payload.waitTime, 0),
1642
+ queueCount: positiveNumber(payload.queueCount, 0),
1643
+ // Only a real boolean is surfaced; a missing field must not be read as
1644
+ // "unavailable" (that would collapse the normal queue into a hard stop).
1645
+ serviceAvailable: typeof payload.serviceAvailable === "boolean" ? payload.serviceAvailable : void 0,
1646
+ // `subCode` sits inside the same record as `isQueued` in the payloads seen
1647
+ // live, so one source is enough; the wrapper levels carry no extra names.
1648
+ subCode: textOf(payload.subCode),
1649
+ queueType: textOf(payload.queueType),
1650
+ modelKey: textOf(payload.modelKey)
1651
+ };
1816
1652
  }
1817
- function resetTimestamp(expiresAt) {
1818
- const ms = quotaNumber(expiresAt);
1819
- if (ms <= 0) return void 0;
1820
- const timestamp = ms < 1e12 ? ms * 1e3 : ms;
1821
- if (new Date(timestamp).getUTCFullYear() >= 9999) return void 0;
1822
- return new Date(timestamp).toISOString();
1653
+ function formatWaitSecondsShort(ms) {
1654
+ if (!Number.isFinite(ms) || ms <= 0) return "0";
1655
+ const seconds = Math.round(ms / 1e3);
1656
+ if (seconds < 100) return String(seconds);
1657
+ return `${formatWaitMinutes(seconds)}\u5206\u949F`;
1658
+ }
1659
+ function formatQueueFailureMessage(notice, ctx) {
1660
+ const isCn = ctx.mode === "cn";
1661
+ const lane = notice.queueType ?? (isCn ? "\u672A\u63D0\u4F9B" : "unset");
1662
+ const sub = notice.subCode ?? (isCn ? "\u672A\u63D0\u4F9B" : "unset");
1663
+ const modelKey = notice.modelKey ?? (isCn ? "\u672A\u63D0\u4F9B" : "unset");
1664
+ const hard = notice.serviceAvailable === false;
1665
+ const waited = formatWaitSecondsShort(ctx.waitedMs ?? 0);
1666
+ const queueLabel = formatQueueFigure(notice.queueCount ?? 0);
1667
+ const tried = ctx.attempts === void 0 ? void 0 : isCn ? `\u5DF2\u5C1D\u8BD5 ${formatQueueFigure(ctx.attempts)} \u6B21\uFF08\u7D2F\u8BA1\u7B49\u5F85 ${waited} \u79D2\uFF09\uFF0C\u7ED3\u8BBA\u76F8\u540C\u3002` : `${formatQueueFigure(ctx.attempts)} attempts (about ${waited} seconds of waiting) ended in the same verdict. `;
1668
+ if (hard) {
1669
+ return isCn ? [
1670
+ `[Qoder CN \u65E0\u53EF\u7528\u5BB9\u91CF] \u7F51\u5173\u56DE serviceAvailable:false\uFF08\u5B50\u7801 ${sub}\uFF0C\u961F\u5217\u957F\u5EA6 ${queueLabel}\uFF0C\u4F18\u5148\u7EA7\u901A\u9053 ${lane}\uFF0C\u6A21\u578B ${modelKey}\uFF09`,
1671
+ `- \u8BF4\u660E\uFF1A${tried ?? ""}\u8FD9\u662F\u8D26\u53F7\u4F18\u5148\u7EA7\u901A\u9053\u7684\u5BB9\u91CF\u9650\u5236\uFF0C\u4E0D\u662F\u51ED\u8BC1\u95EE\u9898\u3002\u6392\u961F\u5C1D\u8BD5\u4E0D\u4EA7\u751F\u8F93\u51FA\uFF0C\u5B9E\u6D4B\u4E5F\u4E0D\u6263 credits\uFF08\u4EE3\u4EF7\u53EA\u6709\u65F6\u5EF6\uFF09\u3002`,
1672
+ `- \u6362\u6A21\u578B\u65E0\u6548\uFF1A\u540C\u4E00\u8D26\u53F7\u5728 ${lane} \u901A\u9053\u4E0B\u5404\u6A21\u578B\u8FD4\u56DE\u540C\u4E00\u7ED3\u8BBA\uFF08\u5B9E\u6D4B auto / qmodel_38max / efficient / ultimate \u4E00\u81F4\uFF09\uFF0C\u8BF7\u52FF\u5728\u540C\u4E00 Provider \u5185\u53CD\u590D\u6311\u6A21\u578B\u3002`,
1673
+ `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u7528 /model \u5207\u6362\u5230 ${ctx.otherProviderID} \u6216\u5176\u4ED6 Provider\uFF0C\u6216\u7A0D\u540E\u91CD\u8BD5\u3002\u989D\u5EA6\u4E0E\u8BA1\u5212\uFF1A${ctx.manageUrl}`
1674
+ ].join("\n") : [
1675
+ `[Qoder international has no spare capacity] The gateway answered serviceAvailable:false (sub code ${sub}, queue length ${queueLabel}, priority lane ${lane}, model ${modelKey})`,
1676
+ `- Meaning: ${tried ?? ""}This is the account's priority-lane capacity, not a credential problem. Queued attempts produce no output and are not billed (measured: 19 queued requests, credits unchanged) \u2014 the only cost is latency.`,
1677
+ `- Switching model will not help: every model of this account answers on the same ${lane} lane (measured across auto / qmodel_38max / efficient / ultimate).`,
1678
+ `- Next step: use /model to switch to ${ctx.otherProviderID} or another provider, or retry later. Plan and quota: ${ctx.manageUrl}`
1679
+ ].join("\n");
1680
+ }
1681
+ return isCn ? [
1682
+ `[Qoder CN \u4E0A\u6E38\u6392\u961F] \u6A21\u578B\u7F51\u5173\u5F53\u524D\u5BB9\u91CF\u5DF2\u6EE1\uFF0C\u8BF7\u6C42\u8FDB\u5165\u7B49\u5F85\u961F\u5217\uFF08\u961F\u5217\u957F\u5EA6 ${queueLabel}\uFF0C\u4F18\u5148\u7EA7\u901A\u9053 ${lane}\uFF0C\u6A21\u578B ${modelKey}\uFF09`,
1683
+ `- \u8BF4\u660E\uFF1A${tried ?? ""}\u961F\u5217\u5C1A\u672A\u653E\u884C\u3002\u51ED\u8BC1\u4E0E\u989D\u5EA6\u6B63\u5E38\uFF0C\u7EAF\u7CB9\u662F\u4E0A\u6E38\u6392\u961F\uFF1B\u6392\u961F\u5C1D\u8BD5\u4E0D\u6D88\u8017 credits\u3002`,
1684
+ `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u7528 /model \u5207\u6362\u5230 ${ctx.otherProviderID} \u7B49\u5176\u4ED6 Provider\u3002\u989D\u5EA6\u4E0E\u8BA1\u5212\uFF1A${ctx.manageUrl}`
1685
+ ].join("\n") : [
1686
+ // "is at capacity" is the established wording for the drainable case and
1687
+ // the only queue text allowed to carry it; the hard refusal above must
1688
+ // never read as "wait it out".
1689
+ `[Qoder upstream queue] The model gateway is at capacity and parked this request (queue length ${queueLabel}, priority lane ${lane}, model ${modelKey})`,
1690
+ `- Meaning: ${tried ?? ""}the queue has not cleared yet. Credentials and quota are fine; queued attempts are not billed.`,
1691
+ `- Next step: retry shortly, or switch provider with /model to ${ctx.otherProviderID} while the queue drains. Plan and quota: ${ctx.manageUrl}`
1692
+ ].join("\n");
1693
+ }
1694
+ function formatLaneCooldownMessage(notice, remainingMs, ctx) {
1695
+ const isCn = ctx.mode === "cn";
1696
+ const lane = notice.queueType ?? (isCn ? "\u672A\u63D0\u4F9B" : "unset");
1697
+ const sub = notice.subCode ?? (isCn ? "\u672A\u63D0\u4F9B" : "unset");
1698
+ const left = Math.min(99, Math.max(1, Math.ceil(remainingMs / 1e3)));
1699
+ return isCn ? [
1700
+ `[Qoder CN \u5DF2\u8DF3\u8FC7\u672C\u6B21\u8BF7\u6C42] \u4F18\u5148\u7EA7\u901A\u9053 ${lane} \u521A\u521A\u56DE\u8FC7 serviceAvailable:false\uFF08\u5B50\u7801 ${sub}\uFF09\uFF0C${left} \u79D2\u5185\u4E0D\u518D\u91CD\u590D\u7B49\u5F85\u3002`,
1701
+ `- \u8BF4\u660E\uFF1A\u8FD9\u6B21\u8BF7\u6C42\u5C1A\u672A\u53D1\u51FA\u3002\u4E0A\u4E00\u8F6E\u540C\u901A\u9053\u8BF7\u6C42\u6309 Retry-After \u7B49\u5F85\u540E\u4ECD\u672A\u653E\u884C\uFF0C\u6392\u961F\u53EA\u4F1A\u518D\u6D88\u8017\u540C\u6837\u7684\u65F6\u5EF6\u3002`,
1702
+ `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u7528 /model \u5207\u6362\u5230 ${ctx.otherProviderID} \u6216\u5176\u4ED6 Provider\uFF0C\u6216 ${left} \u79D2\u540E\u91CD\u8BD5\u3002`
1703
+ ].join("\n") : [
1704
+ `[Qoder request skipped] Priority lane ${lane} just refused service (sub code ${sub}, serviceAvailable:false), so this request was not sent; repeats are skipped for ${left} more seconds.`,
1705
+ `- Meaning: the previous request on this lane already waited through Retry-After and was still refused, so waiting again buys the same verdict.`,
1706
+ `- Next step: use /model to switch to ${ctx.otherProviderID} or another provider, or retry in ${left} seconds.`
1707
+ ].join("\n");
1708
+ }
1709
+ function formatQueueFigure(n) {
1710
+ if (!Number.isFinite(n) || n <= 0) return "0";
1711
+ if (n < 100) return String(Math.round(n));
1712
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
1713
+ return `${(n / 1e3).toFixed(1)}k`;
1714
+ }
1715
+ function formatWaitMinutes(seconds) {
1716
+ if (!Number.isFinite(seconds) || seconds <= 0) return "<1";
1717
+ return String(Math.min(99, Math.max(1, Math.round(seconds / 60))));
1718
+ }
1719
+ function sleep(ms, signal) {
1720
+ return new Promise((resolve, reject) => {
1721
+ if (signal?.aborted) {
1722
+ reject(new Error("Aborted while waiting for the Qoder queue."));
1723
+ return;
1724
+ }
1725
+ let timer;
1726
+ const onAbort = () => {
1727
+ clearTimeout(timer);
1728
+ reject(new Error("Aborted while waiting for the Qoder queue."));
1729
+ };
1730
+ timer = setTimeout(() => {
1731
+ signal?.removeEventListener("abort", onAbort);
1732
+ resolve();
1733
+ }, ms);
1734
+ signal?.addEventListener("abort", onAbort, { once: true });
1735
+ });
1736
+ }
1737
+
1738
+ // src/protocol/errors.ts
1739
+ function parseQoderErrorPayload(payload) {
1740
+ if (!payload) return null;
1741
+ let parsed = payload;
1742
+ if (typeof payload === "string") {
1743
+ try {
1744
+ parsed = JSON.parse(payload);
1745
+ } catch {
1746
+ return null;
1747
+ }
1748
+ }
1749
+ if (typeof parsed !== "object" || parsed === null) return null;
1750
+ const obj = parsed;
1751
+ if (typeof obj.message === "string" && obj.message.trim().startsWith("{")) {
1752
+ try {
1753
+ const nested = JSON.parse(obj.message);
1754
+ if (typeof nested === "object" && nested !== null) {
1755
+ const nestedObj = nested;
1756
+ return {
1757
+ code: String(nestedObj.code || nestedObj.errorCode || obj.code || ""),
1758
+ message: String(nestedObj.message || nestedObj.errorMessage || obj.message || "")
1759
+ };
1760
+ }
1761
+ } catch {
1762
+ }
1763
+ }
1764
+ const code = obj.code ?? obj.errorCode;
1765
+ const message = obj.message ?? obj.errorMessage;
1766
+ if (!code && !message) return null;
1767
+ return {
1768
+ code: code !== void 0 ? String(code) : void 0,
1769
+ message: message !== void 0 ? String(message) : void 0
1770
+ };
1771
+ }
1772
+ function msUntilBeijingMidnight(now = Date.now()) {
1773
+ const beijingOffset = 8 * 36e5;
1774
+ const beijingTime = now + beijingOffset;
1775
+ const beijingDate = new Date(beijingTime);
1776
+ const nextMidnightUtc = Date.UTC(
1777
+ beijingDate.getUTCFullYear(),
1778
+ beijingDate.getUTCMonth(),
1779
+ beijingDate.getUTCDate() + 1,
1780
+ 0,
1781
+ 0,
1782
+ 0
1783
+ );
1784
+ return nextMidnightUtc - beijingTime;
1785
+ }
1786
+ function getBeijingDailyResetCountdown(now = Date.now()) {
1787
+ const diffMs = msUntilBeijingMidnight(now);
1788
+ const totalMinutes = Math.max(1, Math.round(diffMs / 6e4));
1789
+ const hours = Math.floor(totalMinutes / 60);
1790
+ const minutes = totalMinutes % 60;
1791
+ return hours > 0 ? `${hours}\u5C0F\u65F6${minutes}\u5206\u949F` : `${minutes}\u5206\u949F`;
1792
+ }
1793
+ var CREDENTIAL_EXPIRED_CODES = /* @__PURE__ */ new Set(["105", "TOKEN_EXPIRE", "TOKEN_EXPIRED"]);
1794
+ var CREDENTIAL_EXPIRED_MESSAGE = /token expired|login expired|token is not active|token has expired/i;
1795
+ function isCredentialExpiredVerdict(code, message) {
1796
+ return code !== void 0 && CREDENTIAL_EXPIRED_CODES.has(code) || CREDENTIAL_EXPIRED_MESSAGE.test(message);
1797
+ }
1798
+ function isOpenApiInactiveCredential(status, body) {
1799
+ if (status !== 401) return false;
1800
+ const parsed = parseQoderErrorPayload(body);
1801
+ return parsed?.code === "TOKEN_EXPIRE" || /token is not active/i.test(parsed?.message ?? body);
1802
+ }
1803
+ function formatInactiveCredentialMessage(endpoint, accessToken, mode = "cn") {
1804
+ const region = getQoderRegionConfig(mode);
1805
+ const fingerprint = createHash("sha256").update(accessToken).digest("hex").slice(0, 8);
1806
+ const onPublicGateway = endpoint.startsWith(
1807
+ mode === "cn" ? QODER_CN_OFFICIAL.openApiUrl : "https://openapi.qoder.sh"
1808
+ );
1809
+ const where = onPublicGateway ? mode === "cn" ? "\u672C\u8BF7\u6C42\u6253\u5230\u516C\u7F51\u7F51\u5173\uFF0C\u800C\u4E0D\u662F\u4F01\u4E1A\u5B9E\u4F8B\u7684 openapi" : "this request went to the public gateway, not your instance's openapi host" : mode === "cn" ? "\u672C\u8BF7\u6C42\u6253\u5230 VPC \u5B9E\u4F8B\u7684 openapi" : "this request went to a VPC instance's openapi host";
1810
+ const cause = onPublicGateway ? mode === "cn" ? "\u9996\u8981\u6000\u7591\uFF1A\u90E8\u7F72\u4E0D\u5339\u914D\u3002VPC\uFF08\u4F01\u4E1A\uFF09\u8D26\u53F7\u7684 token \u53EA\u5728\u81EA\u5DF1\u5B9E\u4F8B\u7684 openapi \u4E0A\u6709\u6548\uFF0C\u516C\u7F51\u7F51\u5173\u4F1A\u628A\u5B83\u5F53\u6210 not active\uFF1B\u540C\u4E00 token \u7684\u6A21\u578B\u8BF7\u6C42\u4ECD\u53EF\u6B63\u5E38\uFF0C\u56E0\u4E3A\u804A\u5929\u8D70\u7684\u662F\u5DF2\u914D\u7F6E\u7684\u5B9E\u4F8B\u7F51\u5173\u3002\u5728\u8FD9\u79CD\u9519\u914D\u4E0B\u91CD\u65B0\u6362\u53D6 token\uFF08/login \u6B63\u662F\u8FD9\u4E00\u6B65\uFF09\u4E0D\u4F1A\u6D88\u9664 401\u3002" : "most likely a deployment mismatch: a VPC account's token is only active on its own instance's openapi, so the public gateway reports it as not active, while model requests keep working because they use the configured instance gateway. Exchanging a fresh token (what /login does) does not clear that mismatch." : mode === "cn" ? `\u8BE5\u5B9E\u4F8B\u4E0D\u8BA4\u8FD9\u4E2A token\u3002\u786E\u8BA4\u8D26\u53F7\u786E\u5B9E\u5C5E\u4E8E\u8FD9\u4E2A\u5B9E\u4F8B\u3001\u4E14\u5B9E\u4F8B\u672A\u505C\u7528\u5B83\uFF1B\u82E5 endpoint \u914D\u7F6E\u5DF2\u8FC7\u65F6\uFF0C\u8BF7\u7528 /login ${region.providerID} \u91CD\u65B0\u9009\u62E9\u5B9E\u4F8B\u3002` : "this instance does not know the token. Confirm the account belongs to it and that the instance has not revoked the token; if the endpoint setting is stale, reselect it with /login.";
1811
+ const remedy = onPublicGateway ? mode === "cn" ? `\u7528 /login ${region.providerID} \u9009\u62E9\u4F01\u4E1A\u5B9E\u4F8B\uFF08\u6216\u4FEE\u6B63 endpoint\uFF09\u540E\u91CD\u8BD5 /${region.providerID}.usage\u3002` : `select the enterprise instance with /login ${region.providerID} (or fix the endpoint), then retry /${region.providerID}.usage.` : mode === "cn" ? `\u91CD\u8BD5 /${region.providerID}.usage\uFF1B\u4ECD\u88AB\u62D2\u5219\u8FD0\u884C /login ${region.providerID} \u91CD\u65B0\u767B\u5F55\uFF0C\u5E76\u5230 ${region.manageUrl} \u786E\u8BA4 PAT \u672A\u88AB\u540A\u9500\u3002` : `retry /${region.providerID}.usage; if it still refuses, run /login ${region.providerID} and confirm at ${region.manageUrl} that the PAT was not revoked.`;
1812
+ const lines = mode === "cn" ? [
1813
+ `[Qoder CN \u989D\u5EA6\u7AEF\u70B9\u62D2\u7EDD\u5F53\u524D token] code TOKEN_EXPIRE (token is not active)`,
1814
+ `- \u7AEF\u70B9\uFF1A${endpoint} - ${where}`,
1815
+ `- \u539F\u56E0\uFF1A${cause}`,
1816
+ `- \u5904\u7F6E\uFF1A${remedy}`,
1817
+ `- \u8BCA\u65AD\uFF1Atoken sha256:${fingerprint}`
1818
+ ] : [
1819
+ `[Qoder quota endpoint refused this token] code TOKEN_EXPIRE (token is not active)`,
1820
+ `- Endpoint: ${endpoint} - ${where}`,
1821
+ `- Cause: ${cause}`,
1822
+ `- Next step: ${remedy}`,
1823
+ `- Diagnostics: token sha256:${fingerprint}`
1824
+ ];
1825
+ return lines.join("\n");
1826
+ }
1827
+ function formatQoderStreamError(statusCode, rawBody, now = Date.now(), mode = "cn") {
1828
+ const parsed = parseQoderErrorPayload(rawBody);
1829
+ const code = parsed?.code;
1830
+ const message = parsed?.message || (typeof rawBody === "string" ? rawBody : JSON.stringify(rawBody));
1831
+ const bodyForMatch = `${message} ${typeof rawBody === "string" ? rawBody : JSON.stringify(rawBody ?? "")}`;
1832
+ const countdown = getBeijingDailyResetCountdown(now);
1833
+ const isCn = mode !== "global";
1834
+ if (code === "110" || /billing daily count exceeded/i.test(message) || /daily usage limit reached/i.test(message)) {
1835
+ if (!isCn) {
1836
+ return [
1837
+ `[Qoder quota limit] Daily call allowance exhausted (Billing daily count exceeded, code 110)`,
1838
+ `- Reset: at 00:00 UTC+8 (in ${countdown})`,
1839
+ `- 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.`,
1840
+ `- Fix: wait for the reset; switch provider with /model; or upgrade at https://qoder.com/pricing to lift the daily limit.`
1841
+ ].join("\n");
1842
+ }
1843
+ return [
1844
+ `[Qoder CN \u989D\u5EA6\u9650\u5236] \u4ECA\u65E5\u8C03\u7528\u6B21\u6570\u5DF2\u8FBE\u4E0A\u9650 (Billing daily count exceeded, \u9519\u8BEF\u7801 110)`,
1845
+ `- \u5237\u65B0\u65F6\u95F4\uFF1A\u5C06\u5728\u5317\u4EAC\u65F6\u95F4 00:00 \u91CD\u7F6E\uFF08\u7EA6 ${countdown}\u540E\uFF09`,
1846
+ `- \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`,
1847
+ `- \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`
1848
+ ].join("\n");
1849
+ }
1850
+ if (code === "117" || /team member credits exhausted/i.test(message)) {
1851
+ return isCn ? [
1852
+ `[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)`,
1853
+ `- \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`,
1854
+ `- \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`
1855
+ ].join("\n") : [
1856
+ `[Qoder enterprise quota] Your member credit allowance is exhausted (code 117)`,
1857
+ `- Reason: the admin-assigned per-member credit cap has been consumed.`,
1858
+ `- Fix: ask your organization admin to raise your member quota in the team console.`
1859
+ ].join("\n");
1860
+ }
1861
+ if (code === "116" || /team administrator credits exhausted/i.test(message)) {
1862
+ return isCn ? [
1863
+ `[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)`,
1864
+ `- \u9650\u5236\u539F\u56E0\uFF1A\u5F53\u524D\u4F01\u4E1A\u7EC4\u7EC7\u8D26\u6237\u7684 Credits \u989D\u5EA6\u5DF2\u5168\u90E8\u7528\u5B8C\u3002`,
1865
+ `- \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`
1866
+ ].join("\n") : [
1867
+ `[Qoder enterprise quota] The organization's credit pool is exhausted (code 116)`,
1868
+ `- Reason: every credit in the organization account has been consumed.`,
1869
+ `- Fix: ask your organization admin to purchase or renew the org pool.`
1870
+ ].join("\n");
1871
+ }
1872
+ if (code === "122" || /billing-group credits limit reached/i.test(message)) {
1873
+ return isCn ? [
1874
+ `[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)`,
1875
+ `- \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`,
1876
+ `- \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`
1877
+ ].join("\n") : [
1878
+ `[Qoder billing group] The billing group reached its period spend cap (code 122)`,
1879
+ `- Reason: the admin-set limit for this billing group is consumed for the period.`,
1880
+ `- Fix: ask the billing administrator to raise the group's period cap.`
1881
+ ].join("\n");
1882
+ }
1883
+ if (code === "119" || /free usage limit for the selected model reached/i.test(message)) {
1884
+ return isCn ? [
1885
+ `[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)`,
1886
+ `- \u5237\u65B0\u65F6\u95F4\uFF1A\u5C06\u5728\u5317\u4EAC\u65F6\u95F4 00:00 \u91CD\u7F6E\uFF08\u7EA6 ${countdown}\u540E\uFF09`,
1887
+ `- \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`
1888
+ ].join("\n") : [
1889
+ `[Qoder model free tier] This model's free daily trial allowance is used up (code 119)`,
1890
+ `- Reset: at 00:00 UTC+8 (in ${countdown})`,
1891
+ `- Fix: switch to a paid model via /model to spend credits, or wait for the reset.`
1892
+ ].join("\n");
1893
+ }
1894
+ if (code === "113" || code === "118" || /quota exhausted/i.test(message) || /credits exhausted/i.test(message)) {
1895
+ const store = isCn ? QODER_CN_OFFICIAL.manageUrl : "https://qoder.com";
1896
+ return isCn ? [
1897
+ `[Qoder CN \u989D\u5EA6\u8017\u5C3D] \u8D26\u6237 Credits \u4F59\u989D\u5DF2\u5168\u90E8\u7528\u5C3D (\u9519\u8BEF\u7801 ${code || 113})`,
1898
+ `- \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`,
1899
+ `- \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`
1900
+ ].join("\n") : [
1901
+ `[Qoder credits exhausted] The account has no remaining credits (code ${code || 113})`,
1902
+ `- Reason: the plan allowance and every add-on pack are consumed.`,
1903
+ `- Fix: purchase a credit pack at ${store}, or wait for the next billing cycle.`
1904
+ ].join("\n");
1905
+ }
1906
+ if (isCredentialExpiredVerdict(code, message)) {
1907
+ if (isCn) {
1908
+ return [
1909
+ `[Qoder CN \u51ED\u8BC1\u5931\u6548] \u767B\u5F55\u6001\u5DF2\u8FC7\u671F\u6216 Token \u5931\u6548 (\u9519\u8BEF\u7801 105)`,
1910
+ `- \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`
1911
+ ].join("\n");
1912
+ }
1913
+ return [
1914
+ `[Qoder \u51ED\u8BC1\u5931\u6548 | Credential expired] The login session or token has expired (\u9519\u8BEF\u7801 105)`,
1915
+ `- Next step: run /login qoder. You can also refresh credentials by exporting QODER_PERSONAL_ACCESS_TOKEN (PAT).`
1916
+ ].join("\n");
1917
+ }
1918
+ if (code === "103" || /duplicate request/i.test(message)) {
1919
+ return isCn ? [
1920
+ `[Qoder CN \u91CD\u590D\u8BF7\u6C42] \u7F51\u5173\u628A\u8FD9\u6B21\u8BF7\u6C42\u5224\u5B9A\u4E3A\u91CD\u653E\u5E76\u62D2\u7EDD (\u9519\u8BEF\u7801 103 Duplicate request)`,
1921
+ `- \u539F\u56E0\uFF1A\u540C\u4E00\u4E2A\u5DF2\u7B7E\u540D\u7684\u8BF7\u6C42\u88AB\u518D\u6B21\u63D0\u4EA4\uFF08COSY Authorization/\u65F6\u95F4\u6233/\u7B7E\u540D \u672A\u5237\u65B0\uFF09\uFF0C\u5E76\u975E\u989D\u5EA6\u6216\u51ED\u8BC1\u95EE\u9898\u3002`,
1922
+ `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u5BA2\u6237\u7AEF\u5E94\u4E3A\u6BCF\u6B21\u91CD\u8BD5\u91CD\u65B0\u7B7E\u540D\uFF1B\u82E5\u4ECD\u590D\u73B0\uFF0C\u8BF7\u7528 /model \u5207\u6362 Provider \u7A0D\u540E\u91CD\u8BD5\u3002`
1923
+ ].join("\n") : [
1924
+ `[Qoder duplicate request] The gateway rejected this request as a replay (code 103 Duplicate request)`,
1925
+ `- Reason: an already-signed request (same COSY Authorization/timestamp/signature) was submitted again. This is a transient server error, not a quota or credential problem.`,
1926
+ `- Next step: every retry must be re-signed; if it keeps repeating, switch provider with /model and try again shortly.`
1927
+ ].join("\n");
1928
+ }
1929
+ if (code === "10605" || /"isQueued"\s*:\s*true/.test(message)) {
1930
+ const notice = parseQueueNotice(rawBody) ?? parseQueueNotice(message);
1931
+ if (notice) {
1932
+ return formatQueueFailureMessage(notice, {
1933
+ mode,
1934
+ // This call does not own the retry loop, so it claims no attempt count.
1935
+ waitedMs: 0,
1936
+ manageUrl: getQoderRegionConfig(mode).manageUrl,
1937
+ otherProviderID: mode === "cn" ? "qoder" : "qoder-cn"
1938
+ });
1939
+ }
1940
+ return isCn ? [
1941
+ `[Qoder CN \u4E0A\u6E38\u6392\u961F] \u6A21\u578B\u7F51\u5173\u5F53\u524D\u5BB9\u91CF\u5DF2\u6EE1\uFF0C\u8BF7\u6C42\u8FDB\u5165\u7B49\u5F85\u961F\u5217 (\u9519\u8BEF\u7801 10605)`,
1942
+ `- \u8BF4\u660E\uFF1A\u51ED\u8BC1\u4E0E\u989D\u5EA6\u5747\u6B63\u5E38\uFF0C\u7EAF\u7CB9\u662F\u4E0A\u6E38\u6392\u961F\uFF1B\u5DF2\u6309\u7F51\u5173\u7ED9\u51FA\u7684 Retry-After \u91CD\u8BD5\u4ECD\u672A\u653E\u884C\u3002`,
1943
+ `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u7528 /model \u5207\u6362\u5230\u5176\u4ED6 Provider\uFF08\u5982\u5176\u4ED6\u6A21\u578B/\u6E20\u9053\uFF09\u3002`
1944
+ ].join("\n") : [
1945
+ `[Qoder upstream queue] The model gateway is at capacity and parked this request (code 10605 isQueued)`,
1946
+ `- Meaning: credentials and quota are fine \u2014 this is pure upstream queuing; the gateway's own Retry-After wait did not clear it.`,
1947
+ `- Next step: retry shortly, or switch provider with /model while the queue drains.`
1948
+ ].join("\n");
1949
+ }
1950
+ if (code === "1210" || /不支持关闭思考|cannot disable thinking|always.{0,12}think/i.test(bodyForMatch)) {
1951
+ return isCn ? [
1952
+ `[Qoder CN \u4E0D\u652F\u6301\u5173\u95ED\u601D\u8003] \u8BE5\u6A21\u578B\u59CB\u7EC8\u601D\u8003 (\u9519\u8BEF\u7801 1210)`,
1953
+ `- \u8BF4\u660E\uFF1A\u8FD9\u4E2A\u6A21\u578B\u6CA1\u6709\u201C\u5173\u95ED\u601D\u8003\u201D\u9009\u9879\uFF0C\u8BF7\u6C42\u91CC\u4E0D\u80FD\u5E26 enable_thinking=false\u3002`,
1954
+ `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u7528 thinking \u5F00\u5173\u9009 low / medium / high / max \u4EFB\u4E00\u6863\u4F4D\uFF1B\u5BA2\u6237\u7AEF\u5E94\u4E3A\u8BE5\u6A21\u578B\u7701\u7565\u601D\u8003\u5F00\u5173\u3002`
1955
+ ].join("\n") : [
1956
+ `[Qoder model always thinks] This model cannot disable thinking (code 1210)`,
1957
+ `- Meaning: the model has no "off" switch, so a request carrying enable_thinking=false is refused. This will not change on retry.`,
1958
+ `- Next step: pick a thinking level (low / medium / high / max) instead of turning thinking off.`
1959
+ ].join("\n");
1960
+ }
1961
+ if (rawBody === void 0 || rawBody === null) {
1962
+ return `Upstream status ${statusCode}`;
1963
+ }
1964
+ const bodyStr = typeof rawBody === "string" ? rawBody : JSON.stringify(rawBody);
1965
+ const truncated = bodyStr.length > 500 ? `${bodyStr.slice(0, 500)}...` : bodyStr;
1966
+ return `Upstream status ${statusCode}: ${truncated}`;
1967
+ }
1968
+ function isCredentialExpiredError(error) {
1969
+ const message = error instanceof Error ? error.message : String(error ?? "");
1970
+ if (message.includes("\u9519\u8BEF\u7801 105") || message.includes("\u51ED\u8BC1\u5931\u6548")) return true;
1971
+ return isCredentialExpiredVerdict(void 0, message) || /TOKEN_EXPIRED?/i.test(message);
1972
+ }
1973
+
1974
+ // src/quota.ts
1975
+ function quotaNumber(value) {
1976
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
1977
+ }
1978
+ function bucketLimit(bucket) {
1979
+ return quotaNumber(bucket.total ?? bucket.cap);
1980
+ }
1981
+ function hasAllowance(bucket) {
1982
+ if (!bucket) return false;
1983
+ return bucketLimit(bucket) > 0 || quotaNumber(bucket.used) > 0 || quotaNumber(bucket.remaining) > 0;
1984
+ }
1985
+ function pickBucket(raw, camel) {
1986
+ if (!raw) return void 0;
1987
+ const source = raw;
1988
+ const snake = camel.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
1989
+ return source[snake] ?? source[camel];
1990
+ }
1991
+
1992
+ // src/auth/usage.ts
1993
+ init_region();
1994
+ function round2(value) {
1995
+ return Math.round(value * 100) / 100;
1996
+ }
1997
+ function resetTimestamp(expiresAt) {
1998
+ const ms = quotaNumber(expiresAt);
1999
+ if (ms <= 0) return void 0;
2000
+ const timestamp = ms < 1e12 ? ms * 1e3 : ms;
2001
+ if (new Date(timestamp).getUTCFullYear() >= 9999) return void 0;
2002
+ return new Date(timestamp).toISOString();
1823
2003
  }
1824
2004
  async function fetchQoderUsageForMode(credentials, mode) {
1825
2005
  const region = getQoderRegionConfig(mode);
@@ -1832,6 +2012,10 @@ async function fetchQoderUsageForMode(credentials, mode) {
1832
2012
  }
1833
2013
  });
1834
2014
  if (!response.ok) {
2015
+ const body = await response.text().catch(() => "");
2016
+ if (isOpenApiInactiveCredential(response.status, body)) {
2017
+ throw new Error(formatInactiveCredentialMessage(getQoderUsageURL(mode), credentials.access, mode));
2018
+ }
1835
2019
  throw new Error(`Failed to fetch Qoder usage: ${response.status} ${response.statusText}`);
1836
2020
  }
1837
2021
  const raw = await response.json();
@@ -1868,12 +2052,12 @@ init_catalog();
1868
2052
  // src/commands/claim.ts
1869
2053
  init_expiry();
1870
2054
  init_cosy();
1871
- init_region();
1872
2055
  import { spawnSync } from "node:child_process";
1873
2056
  import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
1874
2057
  import { homedir as homedir5, hostname } from "node:os";
1875
2058
  import { basename, dirname as dirname4, join as join5 } from "node:path";
1876
2059
  import { fileURLToPath } from "node:url";
2060
+ init_region();
1877
2061
  function piAgentDir2() {
1878
2062
  const home = process.env.HOME || process.env.USERPROFILE || homedir5();
1879
2063
  return join5(home, ".pi", "agent");
@@ -2034,6 +2218,9 @@ async function fetchQoderCampaigns(accessToken, machineID, mode, identity) {
2034
2218
  data = void 0;
2035
2219
  }
2036
2220
  if (!response.ok) {
2221
+ if (isOpenApiInactiveCredential(response.status, text)) {
2222
+ throw new Error(formatInactiveCredentialMessage(url, accessToken, mode));
2223
+ }
2037
2224
  const detail = data?.message || text.slice(0, 200);
2038
2225
  throw new Error(`Failed to query Qoder campaigns (${response.status}): ${detail}`);
2039
2226
  }
@@ -2056,6 +2243,9 @@ async function claimQoderCampaign(accessToken, machineID, campaignId, mode, iden
2056
2243
  data = void 0;
2057
2244
  }
2058
2245
  if (!response.ok) {
2246
+ if (isOpenApiInactiveCredential(response.status, text)) {
2247
+ throw new Error(formatInactiveCredentialMessage(url, accessToken, mode));
2248
+ }
2059
2249
  const detail = data?.message || text.slice(0, 200);
2060
2250
  throw new Error(`Failed to claim Qoder benefit (${response.status}): ${detail}`);
2061
2251
  }
@@ -2197,414 +2387,139 @@ async function runClaimCommand(mode, args, ctx) {
2197
2387
  if (wantsRaw) {
2198
2388
  const rawJson = JSON.stringify(dailyCreditCampaign, null, 2);
2199
2389
  ctx?.ui?.notify(rawJson, "info");
2200
- if (!ctx?.ui) console.log(rawJson);
2201
- return;
2202
- }
2203
- const lines2 = mode === "cn" ? [
2204
- paint(`\u2139\uFE0F \u4ECA\u65E5 ${amount} Credits \u5DF2\u7ECF\u9886\u53D6\u8FC7\uFF0C\u65E0\u9700\u91CD\u590D\u64CD\u4F5C`, ANSI.cyan),
2205
- `- \u6D3B\u52A8\u540D\u79F0\uFF1A${campaignTitle}`,
2206
- `- \u989D\u5EA6\u8BF4\u660E\uFF1A${amount} Credits\uFF08\u5168\u6A21\u578B\u901A\u7528\u8D44\u6E90\u5305\uFF0C${validityDays} \u5929\u6709\u6548\uFF09`,
2207
- `- \u4E0B\u6B21\u5237\u65B0\uFF1A\u660E\u65E5 10:00 UTC+8\uFF08\u8DDD\u5237\u65B0\u7EA6 ${countdown}\uFF09`
2208
- ] : [
2209
- paint(`\u2139\uFE0F Today's ${amount} Credits already claimed.`, ANSI.cyan),
2210
- `- Campaign: ${campaignTitle}`,
2211
- `- Benefit: ${amount} Credits (${validityDays}-day validity)`,
2212
- `- Next reset: Daily at 10:00 UTC+8 (in ~${countdown})`
2213
- ];
2214
- const output2 = lines2.join("\n");
2215
- ctx?.ui?.notify(output2, "info");
2216
- if (!ctx?.ui) console.log(output2);
2217
- return;
2218
- }
2219
- const claimRes = await claimQoderCampaign(accessToken, machineID, dailyCreditCampaign.campaignId, mode, identity);
2220
- const grantAmount = claimRes.benefit?.amount || amount;
2221
- const grantDays = claimRes.benefit?.validity?.days || validityDays;
2222
- const expiresText = claimRes.expiresAt ? formatDateTime(claimRes.expiresAt) : `${grantDays} \u5929\u540E`;
2223
- writeCheckinCache(
2224
- {
2225
- claimed: true,
2226
- amount: grantAmount,
2227
- claimedAt: claimRes.claimedAt,
2228
- expiresAt: claimRes.expiresAt,
2229
- validityDays: grantDays,
2230
- campaignId: dailyCreditCampaign.campaignId,
2231
- campaignTitle,
2232
- cacheUntil: Date.now() + remainingToReset
2233
- },
2234
- mode
2235
- );
2236
- const lines = mode === "cn" ? [
2237
- paint(`\u{1F389} \u6210\u529F\u9886\u53D6\u4ECA\u65E5 ${grantAmount} Credits\uFF01`, `${ANSI.green}${ANSI.bold}`),
2238
- `- \u989D\u5EA6\u7C7B\u578B\uFF1A\u5168\u6A21\u578B\u901A\u7528\u8D44\u6E90\u5305\uFF08Add-on Credits\uFF09`,
2239
- `- \u6709\u6548\u671F\u9650\uFF1A${grantDays} \u5929\uFF08\u6709\u6548\u671F\u81F3 ${expiresText}\uFF09`,
2240
- `- \u9886\u53D6\u6D41\u6C34\uFF1A${claimRes.grantId || "ok"}`,
2241
- `- \u4E0B\u6B21\u5237\u65B0\uFF1A\u660E\u65E5 10:00 UTC+8\uFF08\u8DDD\u5237\u65B0\u7EA6 ${countdown}\uFF09`
2242
- ] : [
2243
- paint(`\u{1F389} Successfully claimed ${grantAmount} Credits!`, `${ANSI.green}${ANSI.bold}`),
2244
- `- Type: Universal Add-on Credits`,
2245
- `- Validity: ${grantDays} days (expires ${expiresText})`,
2246
- `- Grant ID: ${claimRes.grantId || "ok"}`,
2247
- `- Next reset: Daily at 10:00 UTC+8 (in ~${countdown})`
2248
- ];
2249
- const output = lines.join("\n");
2250
- ctx?.ui?.notify(output, "info");
2251
- if (!ctx?.ui) console.log(output);
2252
- } catch (error) {
2253
- const message = error instanceof Error ? error.message : String(error);
2254
- const errText = mode === "cn" ? `\u274C \u6BCF\u65E5\u7B7E\u5230\u9886\u53D6\u5931\u8D25: ${message}` : `\u274C Daily check-in claim failed: ${message}`;
2255
- ctx?.ui?.notify(errText, "error");
2256
- if (!ctx?.ui) console.error(errText);
2257
- }
2258
- }
2259
-
2260
- // src/protocol/errors.ts
2261
- init_region();
2262
- init_vpc();
2263
-
2264
- // src/protocol/queue.ts
2265
- var MAX_WAIT_MS = 12e4;
2266
- var DEFAULT_RETRY_SECONDS = 30;
2267
- function digForQueue(node, depth) {
2268
- if (depth > 6) return null;
2269
- if (typeof node === "string") {
2270
- const trimmed = node.trim();
2271
- if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return null;
2272
- try {
2273
- return digForQueue(JSON.parse(trimmed), depth + 1);
2274
- } catch {
2275
- return null;
2276
- }
2277
- }
2278
- if (node === null || typeof node !== "object") return null;
2279
- if (Array.isArray(node)) {
2280
- for (const item of node) {
2281
- const found = digForQueue(item, depth + 1);
2282
- if (found) return found;
2283
- }
2284
- return null;
2285
- }
2286
- const record = node;
2287
- if (record.isQueued === true) return record;
2288
- for (const value of Object.values(record)) {
2289
- const found = digForQueue(value, depth + 1);
2290
- if (found) return found;
2291
- }
2292
- return null;
2293
- }
2294
- function positiveNumber(value, fallback) {
2295
- return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
2296
- }
2297
- function textOf(value) {
2298
- return typeof value === "string" && value.trim() ? value.trim() : void 0;
2299
- }
2300
- function parseQueueNotice(envelope) {
2301
- const payload = digForQueue(envelope, 0);
2302
- if (!payload) return null;
2303
- const retryMs = positiveNumber(payload.retry_after_ms, 0) || positiveNumber(payload.retryAfterSeconds, DEFAULT_RETRY_SECONDS) * 1e3;
2304
- return {
2305
- retryAfterMs: Math.min(retryMs, MAX_WAIT_MS),
2306
- waitTimeSeconds: positiveNumber(payload.waitTime, 0),
2307
- queueCount: positiveNumber(payload.queueCount, 0),
2308
- // Only a real boolean is surfaced; a missing field must not be read as
2309
- // "unavailable" (that would collapse the normal queue into a hard stop).
2310
- serviceAvailable: typeof payload.serviceAvailable === "boolean" ? payload.serviceAvailable : void 0,
2311
- // `subCode` sits inside the same record as `isQueued` in the payloads seen
2312
- // live, so one source is enough; the wrapper levels carry no extra names.
2313
- subCode: textOf(payload.subCode),
2314
- queueType: textOf(payload.queueType),
2315
- modelKey: textOf(payload.modelKey)
2316
- };
2317
- }
2318
- function formatWaitSecondsShort(ms) {
2319
- if (!Number.isFinite(ms) || ms <= 0) return "0";
2320
- const seconds = Math.round(ms / 1e3);
2321
- if (seconds < 100) return String(seconds);
2322
- return `${formatWaitMinutes(seconds)}\u5206\u949F`;
2323
- }
2324
- function formatQueueFailureMessage(notice, ctx) {
2325
- const isCn = ctx.mode === "cn";
2326
- const lane = notice.queueType ?? (isCn ? "\u672A\u63D0\u4F9B" : "unset");
2327
- const sub = notice.subCode ?? (isCn ? "\u672A\u63D0\u4F9B" : "unset");
2328
- const modelKey = notice.modelKey ?? (isCn ? "\u672A\u63D0\u4F9B" : "unset");
2329
- const hard = notice.serviceAvailable === false;
2330
- const waited = formatWaitSecondsShort(ctx.waitedMs ?? 0);
2331
- const queueLabel = formatQueueFigure(notice.queueCount ?? 0);
2332
- const tried = ctx.attempts === void 0 ? void 0 : isCn ? `\u5DF2\u5C1D\u8BD5 ${formatQueueFigure(ctx.attempts)} \u6B21\uFF08\u7D2F\u8BA1\u7B49\u5F85 ${waited} \u79D2\uFF09\uFF0C\u7ED3\u8BBA\u76F8\u540C\u3002` : `${formatQueueFigure(ctx.attempts)} attempts (about ${waited} seconds of waiting) ended in the same verdict. `;
2333
- if (hard) {
2334
- return isCn ? [
2335
- `[Qoder CN \u65E0\u53EF\u7528\u5BB9\u91CF] \u7F51\u5173\u56DE serviceAvailable:false\uFF08\u5B50\u7801 ${sub}\uFF0C\u961F\u5217\u957F\u5EA6 ${queueLabel}\uFF0C\u4F18\u5148\u7EA7\u901A\u9053 ${lane}\uFF0C\u6A21\u578B ${modelKey}\uFF09`,
2336
- `- \u8BF4\u660E\uFF1A${tried ?? ""}\u8FD9\u662F\u8D26\u53F7\u4F18\u5148\u7EA7\u901A\u9053\u7684\u5BB9\u91CF\u9650\u5236\uFF0C\u4E0D\u662F\u51ED\u8BC1\u95EE\u9898\u3002\u6392\u961F\u5C1D\u8BD5\u4E0D\u4EA7\u751F\u8F93\u51FA\uFF0C\u5B9E\u6D4B\u4E5F\u4E0D\u6263 credits\uFF08\u4EE3\u4EF7\u53EA\u6709\u65F6\u5EF6\uFF09\u3002`,
2337
- `- \u6362\u6A21\u578B\u65E0\u6548\uFF1A\u540C\u4E00\u8D26\u53F7\u5728 ${lane} \u901A\u9053\u4E0B\u5404\u6A21\u578B\u8FD4\u56DE\u540C\u4E00\u7ED3\u8BBA\uFF08\u5B9E\u6D4B auto / qmodel_38max / efficient / ultimate \u4E00\u81F4\uFF09\uFF0C\u8BF7\u52FF\u5728\u540C\u4E00 Provider \u5185\u53CD\u590D\u6311\u6A21\u578B\u3002`,
2338
- `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u7528 /model \u5207\u6362\u5230 ${ctx.otherProviderID} \u6216\u5176\u4ED6 Provider\uFF0C\u6216\u7A0D\u540E\u91CD\u8BD5\u3002\u989D\u5EA6\u4E0E\u8BA1\u5212\uFF1A${ctx.manageUrl}`
2339
- ].join("\n") : [
2340
- `[Qoder international has no spare capacity] The gateway answered serviceAvailable:false (sub code ${sub}, queue length ${queueLabel}, priority lane ${lane}, model ${modelKey})`,
2341
- `- Meaning: ${tried ?? ""}This is the account's priority-lane capacity, not a credential problem. Queued attempts produce no output and are not billed (measured: 19 queued requests, credits unchanged) \u2014 the only cost is latency.`,
2342
- `- Switching model will not help: every model of this account answers on the same ${lane} lane (measured across auto / qmodel_38max / efficient / ultimate).`,
2343
- `- Next step: use /model to switch to ${ctx.otherProviderID} or another provider, or retry later. Plan and quota: ${ctx.manageUrl}`
2344
- ].join("\n");
2345
- }
2346
- return isCn ? [
2347
- `[Qoder CN \u4E0A\u6E38\u6392\u961F] \u6A21\u578B\u7F51\u5173\u5F53\u524D\u5BB9\u91CF\u5DF2\u6EE1\uFF0C\u8BF7\u6C42\u8FDB\u5165\u7B49\u5F85\u961F\u5217\uFF08\u961F\u5217\u957F\u5EA6 ${queueLabel}\uFF0C\u4F18\u5148\u7EA7\u901A\u9053 ${lane}\uFF0C\u6A21\u578B ${modelKey}\uFF09`,
2348
- `- \u8BF4\u660E\uFF1A${tried ?? ""}\u961F\u5217\u5C1A\u672A\u653E\u884C\u3002\u51ED\u8BC1\u4E0E\u989D\u5EA6\u6B63\u5E38\uFF0C\u7EAF\u7CB9\u662F\u4E0A\u6E38\u6392\u961F\uFF1B\u6392\u961F\u5C1D\u8BD5\u4E0D\u6D88\u8017 credits\u3002`,
2349
- `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u7528 /model \u5207\u6362\u5230 ${ctx.otherProviderID} \u7B49\u5176\u4ED6 Provider\u3002\u989D\u5EA6\u4E0E\u8BA1\u5212\uFF1A${ctx.manageUrl}`
2350
- ].join("\n") : [
2351
- // "is at capacity" is the established wording for the drainable case and
2352
- // the only queue text allowed to carry it; the hard refusal above must
2353
- // never read as "wait it out".
2354
- `[Qoder upstream queue] The model gateway is at capacity and parked this request (queue length ${queueLabel}, priority lane ${lane}, model ${modelKey})`,
2355
- `- Meaning: ${tried ?? ""}the queue has not cleared yet. Credentials and quota are fine; queued attempts are not billed.`,
2356
- `- Next step: retry shortly, or switch provider with /model to ${ctx.otherProviderID} while the queue drains. Plan and quota: ${ctx.manageUrl}`
2357
- ].join("\n");
2358
- }
2359
- function formatLaneCooldownMessage(notice, remainingMs, ctx) {
2360
- const isCn = ctx.mode === "cn";
2361
- const lane = notice.queueType ?? (isCn ? "\u672A\u63D0\u4F9B" : "unset");
2362
- const sub = notice.subCode ?? (isCn ? "\u672A\u63D0\u4F9B" : "unset");
2363
- const left = Math.min(99, Math.max(1, Math.ceil(remainingMs / 1e3)));
2364
- return isCn ? [
2365
- `[Qoder CN \u5DF2\u8DF3\u8FC7\u672C\u6B21\u8BF7\u6C42] \u4F18\u5148\u7EA7\u901A\u9053 ${lane} \u521A\u521A\u56DE\u8FC7 serviceAvailable:false\uFF08\u5B50\u7801 ${sub}\uFF09\uFF0C${left} \u79D2\u5185\u4E0D\u518D\u91CD\u590D\u7B49\u5F85\u3002`,
2366
- `- \u8BF4\u660E\uFF1A\u8FD9\u6B21\u8BF7\u6C42\u5C1A\u672A\u53D1\u51FA\u3002\u4E0A\u4E00\u8F6E\u540C\u901A\u9053\u8BF7\u6C42\u6309 Retry-After \u7B49\u5F85\u540E\u4ECD\u672A\u653E\u884C\uFF0C\u6392\u961F\u53EA\u4F1A\u518D\u6D88\u8017\u540C\u6837\u7684\u65F6\u5EF6\u3002`,
2367
- `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u7528 /model \u5207\u6362\u5230 ${ctx.otherProviderID} \u6216\u5176\u4ED6 Provider\uFF0C\u6216 ${left} \u79D2\u540E\u91CD\u8BD5\u3002`
2368
- ].join("\n") : [
2369
- `[Qoder request skipped] Priority lane ${lane} just refused service (sub code ${sub}, serviceAvailable:false), so this request was not sent; repeats are skipped for ${left} more seconds.`,
2370
- `- Meaning: the previous request on this lane already waited through Retry-After and was still refused, so waiting again buys the same verdict.`,
2371
- `- Next step: use /model to switch to ${ctx.otherProviderID} or another provider, or retry in ${left} seconds.`
2372
- ].join("\n");
2373
- }
2374
- function formatQueueFigure(n) {
2375
- if (!Number.isFinite(n) || n <= 0) return "0";
2376
- if (n < 100) return String(Math.round(n));
2377
- if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
2378
- return `${(n / 1e3).toFixed(1)}k`;
2379
- }
2380
- function formatWaitMinutes(seconds) {
2381
- if (!Number.isFinite(seconds) || seconds <= 0) return "<1";
2382
- return String(Math.min(99, Math.max(1, Math.round(seconds / 60))));
2383
- }
2384
- function sleep(ms, signal) {
2385
- return new Promise((resolve, reject) => {
2386
- if (signal?.aborted) {
2387
- reject(new Error("Aborted while waiting for the Qoder queue."));
2388
- return;
2389
- }
2390
- let timer;
2391
- const onAbort = () => {
2392
- clearTimeout(timer);
2393
- reject(new Error("Aborted while waiting for the Qoder queue."));
2394
- };
2395
- timer = setTimeout(() => {
2396
- signal?.removeEventListener("abort", onAbort);
2397
- resolve();
2398
- }, ms);
2399
- signal?.addEventListener("abort", onAbort, { once: true });
2400
- });
2401
- }
2402
-
2403
- // src/protocol/errors.ts
2404
- function parseQoderErrorPayload(payload) {
2405
- if (!payload) return null;
2406
- let parsed = payload;
2407
- if (typeof payload === "string") {
2408
- try {
2409
- parsed = JSON.parse(payload);
2410
- } catch {
2411
- return null;
2412
- }
2413
- }
2414
- if (typeof parsed !== "object" || parsed === null) return null;
2415
- const obj = parsed;
2416
- if (typeof obj.message === "string" && obj.message.trim().startsWith("{")) {
2417
- try {
2418
- const nested = JSON.parse(obj.message);
2419
- if (typeof nested === "object" && nested !== null) {
2420
- const nestedObj = nested;
2421
- return {
2422
- code: String(nestedObj.code || nestedObj.errorCode || obj.code || ""),
2423
- message: String(nestedObj.message || nestedObj.errorMessage || obj.message || "")
2424
- };
2390
+ if (!ctx?.ui) console.log(rawJson);
2391
+ return;
2425
2392
  }
2426
- } catch {
2393
+ const lines2 = mode === "cn" ? [
2394
+ paint(`\u2139\uFE0F \u4ECA\u65E5 ${amount} Credits \u5DF2\u7ECF\u9886\u53D6\u8FC7\uFF0C\u65E0\u9700\u91CD\u590D\u64CD\u4F5C`, ANSI.cyan),
2395
+ `- \u6D3B\u52A8\u540D\u79F0\uFF1A${campaignTitle}`,
2396
+ `- \u989D\u5EA6\u8BF4\u660E\uFF1A${amount} Credits\uFF08\u5168\u6A21\u578B\u901A\u7528\u8D44\u6E90\u5305\uFF0C${validityDays} \u5929\u6709\u6548\uFF09`,
2397
+ `- \u4E0B\u6B21\u5237\u65B0\uFF1A\u660E\u65E5 10:00 UTC+8\uFF08\u8DDD\u5237\u65B0\u7EA6 ${countdown}\uFF09`
2398
+ ] : [
2399
+ paint(`\u2139\uFE0F Today's ${amount} Credits already claimed.`, ANSI.cyan),
2400
+ `- Campaign: ${campaignTitle}`,
2401
+ `- Benefit: ${amount} Credits (${validityDays}-day validity)`,
2402
+ `- Next reset: Daily at 10:00 UTC+8 (in ~${countdown})`
2403
+ ];
2404
+ const output2 = lines2.join("\n");
2405
+ ctx?.ui?.notify(output2, "info");
2406
+ if (!ctx?.ui) console.log(output2);
2407
+ return;
2427
2408
  }
2409
+ const claimRes = await claimQoderCampaign(accessToken, machineID, dailyCreditCampaign.campaignId, mode, identity);
2410
+ const grantAmount = claimRes.benefit?.amount || amount;
2411
+ const grantDays = claimRes.benefit?.validity?.days || validityDays;
2412
+ const expiresText = claimRes.expiresAt ? formatDateTime(claimRes.expiresAt) : `${grantDays} \u5929\u540E`;
2413
+ writeCheckinCache(
2414
+ {
2415
+ claimed: true,
2416
+ amount: grantAmount,
2417
+ claimedAt: claimRes.claimedAt,
2418
+ expiresAt: claimRes.expiresAt,
2419
+ validityDays: grantDays,
2420
+ campaignId: dailyCreditCampaign.campaignId,
2421
+ campaignTitle,
2422
+ cacheUntil: Date.now() + remainingToReset
2423
+ },
2424
+ mode
2425
+ );
2426
+ const lines = mode === "cn" ? [
2427
+ paint(`\u{1F389} \u6210\u529F\u9886\u53D6\u4ECA\u65E5 ${grantAmount} Credits\uFF01`, `${ANSI.green}${ANSI.bold}`),
2428
+ `- \u989D\u5EA6\u7C7B\u578B\uFF1A\u5168\u6A21\u578B\u901A\u7528\u8D44\u6E90\u5305\uFF08Add-on Credits\uFF09`,
2429
+ `- \u6709\u6548\u671F\u9650\uFF1A${grantDays} \u5929\uFF08\u6709\u6548\u671F\u81F3 ${expiresText}\uFF09`,
2430
+ `- \u9886\u53D6\u6D41\u6C34\uFF1A${claimRes.grantId || "ok"}`,
2431
+ `- \u4E0B\u6B21\u5237\u65B0\uFF1A\u660E\u65E5 10:00 UTC+8\uFF08\u8DDD\u5237\u65B0\u7EA6 ${countdown}\uFF09`
2432
+ ] : [
2433
+ paint(`\u{1F389} Successfully claimed ${grantAmount} Credits!`, `${ANSI.green}${ANSI.bold}`),
2434
+ `- Type: Universal Add-on Credits`,
2435
+ `- Validity: ${grantDays} days (expires ${expiresText})`,
2436
+ `- Grant ID: ${claimRes.grantId || "ok"}`,
2437
+ `- Next reset: Daily at 10:00 UTC+8 (in ~${countdown})`
2438
+ ];
2439
+ const output = lines.join("\n");
2440
+ ctx?.ui?.notify(output, "info");
2441
+ if (!ctx?.ui) console.log(output);
2442
+ } catch (error) {
2443
+ const message = error instanceof Error ? error.message : String(error);
2444
+ const errText = mode === "cn" ? `\u274C \u6BCF\u65E5\u7B7E\u5230\u9886\u53D6\u5931\u8D25: ${message}` : `\u274C Daily check-in claim failed: ${message}`;
2445
+ ctx?.ui?.notify(errText, "error");
2446
+ if (!ctx?.ui) console.error(errText);
2428
2447
  }
2429
- const code = obj.code ?? obj.errorCode;
2430
- const message = obj.message ?? obj.errorMessage;
2431
- if (!code && !message) return null;
2432
- return {
2433
- code: code !== void 0 ? String(code) : void 0,
2434
- message: message !== void 0 ? String(message) : void 0
2435
- };
2436
- }
2437
- function msUntilBeijingMidnight(now = Date.now()) {
2438
- const beijingOffset = 8 * 36e5;
2439
- const beijingTime = now + beijingOffset;
2440
- const beijingDate = new Date(beijingTime);
2441
- const nextMidnightUtc = Date.UTC(
2442
- beijingDate.getUTCFullYear(),
2443
- beijingDate.getUTCMonth(),
2444
- beijingDate.getUTCDate() + 1,
2445
- 0,
2446
- 0,
2447
- 0
2448
- );
2449
- return nextMidnightUtc - beijingTime;
2450
- }
2451
- function getBeijingDailyResetCountdown(now = Date.now()) {
2452
- const diffMs = msUntilBeijingMidnight(now);
2453
- const totalMinutes = Math.max(1, Math.round(diffMs / 6e4));
2454
- const hours = Math.floor(totalMinutes / 60);
2455
- const minutes = totalMinutes % 60;
2456
- return hours > 0 ? `${hours}\u5C0F\u65F6${minutes}\u5206\u949F` : `${minutes}\u5206\u949F`;
2457
- }
2458
- var CREDENTIAL_EXPIRED_CODES = /* @__PURE__ */ new Set(["105", "TOKEN_EXPIRE", "TOKEN_EXPIRED"]);
2459
- var CREDENTIAL_EXPIRED_MESSAGE = /token expired|login expired|token is not active|token has expired/i;
2460
- function isCredentialExpiredVerdict(code, message) {
2461
- return code !== void 0 && CREDENTIAL_EXPIRED_CODES.has(code) || CREDENTIAL_EXPIRED_MESSAGE.test(message);
2462
2448
  }
2463
- function formatQoderStreamError(statusCode, rawBody, now = Date.now(), mode = "cn") {
2464
- const parsed = parseQoderErrorPayload(rawBody);
2465
- const code = parsed?.code;
2466
- const message = parsed?.message || (typeof rawBody === "string" ? rawBody : JSON.stringify(rawBody));
2467
- const bodyForMatch = `${message} ${typeof rawBody === "string" ? rawBody : JSON.stringify(rawBody ?? "")}`;
2468
- const countdown = getBeijingDailyResetCountdown(now);
2469
- const isCn = mode !== "global";
2470
- if (code === "110" || /billing daily count exceeded/i.test(message) || /daily usage limit reached/i.test(message)) {
2471
- if (!isCn) {
2472
- return [
2473
- `[Qoder quota limit] Daily call allowance exhausted (Billing daily count exceeded, code 110)`,
2474
- `- Reset: at 00:00 UTC+8 (in ${countdown})`,
2475
- `- 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.`,
2476
- `- Fix: wait for the reset; switch provider with /model; or upgrade at https://qoder.com/pricing to lift the daily limit.`
2477
- ].join("\n");
2478
- }
2479
- return [
2480
- `[Qoder CN \u989D\u5EA6\u9650\u5236] \u4ECA\u65E5\u8C03\u7528\u6B21\u6570\u5DF2\u8FBE\u4E0A\u9650 (Billing daily count exceeded, \u9519\u8BEF\u7801 110)`,
2481
- `- \u5237\u65B0\u65F6\u95F4\uFF1A\u5C06\u5728\u5317\u4EAC\u65F6\u95F4 00:00 \u91CD\u7F6E\uFF08\u7EA6 ${countdown}\u540E\uFF09`,
2482
- `- \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`,
2483
- `- \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`
2484
- ].join("\n");
2485
- }
2486
- if (code === "117" || /team member credits exhausted/i.test(message)) {
2487
- return isCn ? [
2488
- `[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)`,
2489
- `- \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`,
2490
- `- \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`
2491
- ].join("\n") : [
2492
- `[Qoder enterprise quota] Your member credit allowance is exhausted (code 117)`,
2493
- `- Reason: the admin-assigned per-member credit cap has been consumed.`,
2494
- `- Fix: ask your organization admin to raise your member quota in the team console.`
2495
- ].join("\n");
2449
+
2450
+ // src/commands/models.ts
2451
+ init_catalog();
2452
+ init_region();
2453
+ function formatCatalogDiff(diff, mode, color = false) {
2454
+ const region = getQoderRegionConfig(mode);
2455
+ const paint3 = (text, code) => color ? `${code}${text}\x1B[0m` : text;
2456
+ const lines = [`${region.usageTitle} models \xB7 ${diff.total} offered by the catalog`];
2457
+ if (diff.added.length > 0) {
2458
+ lines.push("", paint3(`New (${diff.added.length})`, "\x1B[32m"));
2459
+ for (const model of diff.added) lines.push(` + ${model.id} ${model.name}`);
2496
2460
  }
2497
- if (code === "116" || /team administrator credits exhausted/i.test(message)) {
2498
- return isCn ? [
2499
- `[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)`,
2500
- `- \u9650\u5236\u539F\u56E0\uFF1A\u5F53\u524D\u4F01\u4E1A\u7EC4\u7EC7\u8D26\u6237\u7684 Credits \u989D\u5EA6\u5DF2\u5168\u90E8\u7528\u5B8C\u3002`,
2501
- `- \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`
2502
- ].join("\n") : [
2503
- `[Qoder enterprise quota] The organization's credit pool is exhausted (code 116)`,
2504
- `- Reason: every credit in the organization account has been consumed.`,
2505
- `- Fix: ask your organization admin to purchase or renew the org pool.`
2506
- ].join("\n");
2461
+ if (diff.renamed.length > 0) {
2462
+ lines.push("", paint3(`Renamed upstream (${diff.renamed.length})`, "\x1B[33m"));
2463
+ for (const model of diff.renamed) lines.push(` ~ ${model.id} ${model.from} -> ${model.to}`);
2507
2464
  }
2508
- if (code === "122" || /billing-group credits limit reached/i.test(message)) {
2509
- return isCn ? [
2510
- `[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)`,
2511
- `- \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`,
2512
- `- \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`
2513
- ].join("\n") : [
2514
- `[Qoder billing group] The billing group reached its period spend cap (code 122)`,
2515
- `- Reason: the admin-set limit for this billing group is consumed for the period.`,
2516
- `- Fix: ask the billing administrator to raise the group's period cap.`
2517
- ].join("\n");
2465
+ if (diff.removed.length > 0) {
2466
+ lines.push("", paint3(`Gone (${diff.removed.length})`, "\x1B[31m"));
2467
+ for (const model of diff.removed) lines.push(` - ${model.id} ${model.name}`);
2518
2468
  }
2519
- if (code === "119" || /free usage limit for the selected model reached/i.test(message)) {
2520
- return isCn ? [
2521
- `[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)`,
2522
- `- \u5237\u65B0\u65F6\u95F4\uFF1A\u5C06\u5728\u5317\u4EAC\u65F6\u95F4 00:00 \u91CD\u7F6E\uFF08\u7EA6 ${countdown}\u540E\uFF09`,
2523
- `- \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`
2524
- ].join("\n") : [
2525
- `[Qoder model free tier] This model's free daily trial allowance is used up (code 119)`,
2526
- `- Reset: at 00:00 UTC+8 (in ${countdown})`,
2527
- `- Fix: switch to a paid model via /model to spend credits, or wait for the reset.`
2528
- ].join("\n");
2469
+ if (diff.added.length === 0 && diff.renamed.length === 0 && diff.removed.length === 0) {
2470
+ lines.push("", "No change since the last refresh.");
2529
2471
  }
2530
- if (code === "113" || code === "118" || /quota exhausted/i.test(message) || /credits exhausted/i.test(message)) {
2531
- const store = isCn ? QODER_CN_OFFICIAL.manageUrl : "https://qoder.com";
2532
- return isCn ? [
2533
- `[Qoder CN \u989D\u5EA6\u8017\u5C3D] \u8D26\u6237 Credits \u4F59\u989D\u5DF2\u5168\u90E8\u7528\u5C3D (\u9519\u8BEF\u7801 ${code || 113})`,
2534
- `- \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`,
2535
- `- \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`
2536
- ].join("\n") : [
2537
- `[Qoder credits exhausted] The account has no remaining credits (code ${code || 113})`,
2538
- `- Reason: the plan allowance and every add-on pack are consumed.`,
2539
- `- Fix: purchase a credit pack at ${store}, or wait for the next billing cycle.`
2540
- ].join("\n");
2472
+ lines.push("", `Provider list refreshed in this session; /model now shows ${diff.total} entries.`);
2473
+ return lines;
2474
+ }
2475
+ async function refreshCatalog(mode, accessToken, identity, refreshProvider) {
2476
+ const region = getQoderRegionConfig(mode);
2477
+ clearQoderModelsMemCache();
2478
+ const diff = await updateQoderModelsCache(
2479
+ accessToken,
2480
+ identity.userID || "qoder-user",
2481
+ identity.name || region.userNameFallback,
2482
+ identity.email || region.userEmailFallback,
2483
+ mode
2484
+ );
2485
+ if (diff) {
2486
+ clearQoderModelsMemCache();
2487
+ refreshProvider?.();
2541
2488
  }
2542
- if (isCredentialExpiredVerdict(code, message)) {
2543
- if (isCn) {
2544
- return [
2545
- `[Qoder CN \u51ED\u8BC1\u5931\u6548] \u767B\u5F55\u6001\u5DF2\u8FC7\u671F\u6216 Token \u5931\u6548 (\u9519\u8BEF\u7801 105)`,
2546
- `- \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`
2547
- ].join("\n");
2489
+ return diff;
2490
+ }
2491
+ async function runModelsCommand(mode, args, ctx, refreshProvider) {
2492
+ const region = getQoderRegionConfig(mode);
2493
+ const providerID = region.providerID;
2494
+ const target = (args || "").trim().toLowerCase();
2495
+ const plain = target === "plain";
2496
+ const wantsList = target === "list";
2497
+ try {
2498
+ const accessToken = await ctx?.modelRegistry?.getApiKeyForProvider(providerID);
2499
+ if (!accessToken) {
2500
+ ctx?.ui?.notify(`No ${providerID} credentials. Run /login ${providerID} first.`, "warning");
2501
+ return;
2548
2502
  }
2549
- return [
2550
- `[Qoder \u51ED\u8BC1\u5931\u6548 | Credential expired] The login session or token has expired (\u9519\u8BEF\u7801 105)`,
2551
- `- Next step: run /login qoder. You can also refresh credentials by exporting QODER_PERSONAL_ACCESS_TOKEN (PAT).`
2552
- ].join("\n");
2553
- }
2554
- if (code === "103" || /duplicate request/i.test(message)) {
2555
- return isCn ? [
2556
- `[Qoder CN \u91CD\u590D\u8BF7\u6C42] \u7F51\u5173\u628A\u8FD9\u6B21\u8BF7\u6C42\u5224\u5B9A\u4E3A\u91CD\u653E\u5E76\u62D2\u7EDD (\u9519\u8BEF\u7801 103 Duplicate request)`,
2557
- `- \u539F\u56E0\uFF1A\u540C\u4E00\u4E2A\u5DF2\u7B7E\u540D\u7684\u8BF7\u6C42\u88AB\u518D\u6B21\u63D0\u4EA4\uFF08COSY Authorization/\u65F6\u95F4\u6233/\u7B7E\u540D \u672A\u5237\u65B0\uFF09\uFF0C\u5E76\u975E\u989D\u5EA6\u6216\u51ED\u8BC1\u95EE\u9898\u3002`,
2558
- `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u5BA2\u6237\u7AEF\u5E94\u4E3A\u6BCF\u6B21\u91CD\u8BD5\u91CD\u65B0\u7B7E\u540D\uFF1B\u82E5\u4ECD\u590D\u73B0\uFF0C\u8BF7\u7528 /model \u5207\u6362 Provider \u7A0D\u540E\u91CD\u8BD5\u3002`
2559
- ].join("\n") : [
2560
- `[Qoder duplicate request] The gateway rejected this request as a replay (code 103 Duplicate request)`,
2561
- `- Reason: an already-signed request (same COSY Authorization/timestamp/signature) was submitted again. This is a transient server error, not a quota or credential problem.`,
2562
- `- Next step: every retry must be re-signed; if it keeps repeating, switch provider with /model and try again shortly.`
2563
- ].join("\n");
2564
- }
2565
- if (code === "10605" || /"isQueued"\s*:\s*true/.test(message)) {
2566
- const notice = parseQueueNotice(rawBody) ?? parseQueueNotice(message);
2567
- if (notice) {
2568
- return formatQueueFailureMessage(notice, {
2569
- mode,
2570
- // This call does not own the retry loop, so it claims no attempt count.
2571
- waitedMs: 0,
2572
- manageUrl: getQoderRegionConfig(mode).manageUrl,
2573
- otherProviderID: mode === "cn" ? "qoder" : "qoder-cn"
2574
- });
2503
+ if (wantsList) {
2504
+ const models = getCachedModels(mode);
2505
+ ctx?.ui?.notify(
2506
+ [`${region.usageTitle} models`, ...models.map((model) => ` ${model.id} ${model.name}`)].join("\n")
2507
+ );
2508
+ return;
2575
2509
  }
2576
- return isCn ? [
2577
- `[Qoder CN \u4E0A\u6E38\u6392\u961F] \u6A21\u578B\u7F51\u5173\u5F53\u524D\u5BB9\u91CF\u5DF2\u6EE1\uFF0C\u8BF7\u6C42\u8FDB\u5165\u7B49\u5F85\u961F\u5217 (\u9519\u8BEF\u7801 10605)`,
2578
- `- \u8BF4\u660E\uFF1A\u51ED\u8BC1\u4E0E\u989D\u5EA6\u5747\u6B63\u5E38\uFF0C\u7EAF\u7CB9\u662F\u4E0A\u6E38\u6392\u961F\uFF1B\u5DF2\u6309\u7F51\u5173\u7ED9\u51FA\u7684 Retry-After \u91CD\u8BD5\u4ECD\u672A\u653E\u884C\u3002`,
2579
- `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u7528 /model \u5207\u6362\u5230\u5176\u4ED6 Provider\uFF08\u5982\u5176\u4ED6\u6A21\u578B/\u6E20\u9053\uFF09\u3002`
2580
- ].join("\n") : [
2581
- `[Qoder upstream queue] The model gateway is at capacity and parked this request (code 10605 isQueued)`,
2582
- `- Meaning: credentials and quota are fine \u2014 this is pure upstream queuing; the gateway's own Retry-After wait did not clear it.`,
2583
- `- Next step: retry shortly, or switch provider with /model while the queue drains.`
2584
- ].join("\n");
2585
- }
2586
- if (code === "1210" || /不支持关闭思考|cannot disable thinking|always.{0,12}think/i.test(bodyForMatch)) {
2587
- return isCn ? [
2588
- `[Qoder CN \u4E0D\u652F\u6301\u5173\u95ED\u601D\u8003] \u8BE5\u6A21\u578B\u59CB\u7EC8\u601D\u8003 (\u9519\u8BEF\u7801 1210)`,
2589
- `- \u8BF4\u660E\uFF1A\u8FD9\u4E2A\u6A21\u578B\u6CA1\u6709\u201C\u5173\u95ED\u601D\u8003\u201D\u9009\u9879\uFF0C\u8BF7\u6C42\u91CC\u4E0D\u80FD\u5E26 enable_thinking=false\u3002`,
2590
- `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u7528 thinking \u5F00\u5173\u9009 low / medium / high / max \u4EFB\u4E00\u6863\u4F4D\uFF1B\u5BA2\u6237\u7AEF\u5E94\u4E3A\u8BE5\u6A21\u578B\u7701\u7565\u601D\u8003\u5F00\u5173\u3002`
2591
- ].join("\n") : [
2592
- `[Qoder model always thinks] This model cannot disable thinking (code 1210)`,
2593
- `- Meaning: the model has no "off" switch, so a request carrying enable_thinking=false is refused. This will not change on retry.`,
2594
- `- Next step: pick a thinking level (low / medium / high / max) instead of turning thinking off.`
2595
- ].join("\n");
2596
- }
2597
- if (rawBody === void 0 || rawBody === null) {
2598
- return `Upstream status ${statusCode}`;
2510
+ const diff = await refreshCatalog(mode, accessToken, {}, refreshProvider);
2511
+ if (!diff) {
2512
+ ctx?.ui?.notify(
2513
+ `Could not refresh the ${providerID} model catalog (network, credentials, or an unexpected payload). The existing list is unchanged.`,
2514
+ "warning"
2515
+ );
2516
+ return;
2517
+ }
2518
+ ctx?.ui?.notify(formatCatalogDiff(diff, mode, !plain).join("\n"));
2519
+ } catch (error) {
2520
+ const message = error instanceof Error ? error.message : String(error);
2521
+ ctx?.ui?.notify(`Model refresh failed: ${message}`, "error");
2599
2522
  }
2600
- const bodyStr = typeof rawBody === "string" ? rawBody : JSON.stringify(rawBody);
2601
- const truncated = bodyStr.length > 500 ? `${bodyStr.slice(0, 500)}...` : bodyStr;
2602
- return `Upstream status ${statusCode}: ${truncated}`;
2603
- }
2604
- function isCredentialExpiredError(error) {
2605
- const message = error instanceof Error ? error.message : String(error ?? "");
2606
- if (message.includes("\u9519\u8BEF\u7801 105") || message.includes("\u51ED\u8BC1\u5931\u6548")) return true;
2607
- return isCredentialExpiredVerdict(void 0, message) || /TOKEN_EXPIRED?/i.test(message);
2608
2523
  }
2609
2524
 
2610
2525
  // src/commands/usage.ts
@@ -2615,7 +2530,7 @@ function positiveOr(value, fallback) {
2615
2530
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
2616
2531
  }
2617
2532
  function estimateRollingAddOnExpiry(addOn, grant, now = Date.now()) {
2618
- if (!addOn || !addOn.total || addOn.total <= 0 || !addOn.remaining || addOn.remaining <= 0) {
2533
+ if (!addOn?.total || addOn.total <= 0 || !addOn.remaining || addOn.remaining <= 0) {
2619
2534
  return null;
2620
2535
  }
2621
2536
  const shape = typeof grant === "number" ? { expiryMs: grant } : grant ?? {};
@@ -2858,6 +2773,9 @@ async function fetchQoderQuota(accessToken, mode) {
2858
2773
  data = void 0;
2859
2774
  }
2860
2775
  if (!response.ok) {
2776
+ if (isOpenApiInactiveCredential(response.status, text)) {
2777
+ throw new Error(formatInactiveCredentialMessage(getQoderUsageURL(mode), accessToken, mode));
2778
+ }
2861
2779
  const detail = data?.message || data?.error || text.slice(0, 200);
2862
2780
  const hint = response.status === 401 || response.status === 403 ? `Re-login with /login ${getQoderRegionConfig(mode).providerID}.` : "";
2863
2781
  const parts = [`Qoder usage request failed: ${response.status} ${response.statusText}`, hint, detail];
@@ -4115,13 +4033,16 @@ function registerQoderProvider(pi, mode) {
4115
4033
  streamSimple: streamQoder
4116
4034
  });
4117
4035
  }
4036
+ function reregisterQoderProvider(pi, mode) {
4037
+ registerQoderProvider(pi, mode);
4038
+ }
4118
4039
  async function refreshModelsAtStartup(mode) {
4119
4040
  const providerID = getQoderRegionConfig(mode).providerID;
4120
- if (!isCacheStale(mode)) return;
4041
+ if (!isCacheStale(mode)) return null;
4121
4042
  const credentials = getCachedCredentials("", providerID);
4122
- if (!credentials?.access) return;
4043
+ if (!credentials?.access) return null;
4123
4044
  const region = getQoderRegionConfig(mode);
4124
- await updateQoderModelsCache(
4045
+ return updateQoderModelsCache(
4125
4046
  credentials.access,
4126
4047
  credentials.userID || "qoder-user",
4127
4048
  credentials.name || region.userNameFallback,
@@ -4173,6 +4094,12 @@ function registerCommands(pi) {
4173
4094
  await runClaimCommand("cn", args, ctx);
4174
4095
  }
4175
4096
  });
4097
+ pi.registerCommand("qoder-cn.models", {
4098
+ description: "Refresh the Qoder CN model catalog and report new, renamed and retired models (append 'list' for the cached list)",
4099
+ handler: async (args, ctx) => {
4100
+ await runModelsCommand("cn", args, ctx, () => reregisterQoderProvider(pi, "cn"));
4101
+ }
4102
+ });
4176
4103
  pi.registerCommand("qoder.usage", {
4177
4104
  description: "Show Qoder Global quota: plan + add-on credits and check-in status (append 'json' for raw payload)",
4178
4105
  handler: async (args, ctx) => {
@@ -4185,6 +4112,12 @@ function registerCommands(pi) {
4185
4112
  await runClaimCommand("global", args, ctx);
4186
4113
  }
4187
4114
  });
4115
+ pi.registerCommand("qoder.models", {
4116
+ description: "Refresh the Qoder Global model catalog and report new, renamed and retired models (append 'list' for the cached list)",
4117
+ handler: async (args, ctx) => {
4118
+ await runModelsCommand("global", args, ctx, () => reregisterQoderProvider(pi, "global"));
4119
+ }
4120
+ });
4188
4121
  }
4189
4122
  export {
4190
4123
  index_default as default