pi-provider-qoder-cn 0.2.25 → 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 +31 -3
  2. package/dist/index.js +135 -245
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -25,14 +25,15 @@ pi install npm:pi-provider-qoder-cn
25
25
  | Seamless token refresh (deviceToken/refresh for drt- & PAT exchange) | ❌ (broken 403) | ✅ |
26
26
  | Enterprise (VPC) selection during `/login qoder-cn` | ❌ | ✅ |
27
27
  | Current CN model catalog (`qwen3.8-max` / `qwen3.8-flash`, …) | ❌ | ✅ |
28
- | Stable CN model ids for `enabledModels` | ❌ | ✅ |
28
+ | Model ids follow the live catalog (an upgrade gets a new id; retired models disappear) | ❌ | ✅ |
29
+ | `/qoder-cn.models` & `/qoder.models` manual catalog refresh | ❌ | ✅ |
29
30
  | Capacity-queue detection (`isQueued` / `RESOURCE_LIMIT` / lane) | ❌ | ✅ (recognised, honoured, adjustable) |
30
31
  | Per-turn wait budget + lane cooldown knobs | ❌ | ✅ |
31
32
  | Cache ownership tag (foreign catalogs discarded, not half-read) | ❌ | ✅ |
32
33
 
33
34
  Both `qoder` (Global) and `qoder-cn` (Domestic) providers are registered.
34
- - To use Global: run `/login qoder` and use `/model qoder/<id>`. Commands: `/qoder.usage`, `/qoder.claim`.
35
- - To use CN: run `/login qoder-cn` and use `/model qoder-cn/<id>`. Commands: `/qoder-cn.usage`, `/qoder-cn.claim`.
35
+ - To use Global: run `/login qoder` and use `/model qoder/<id>`. Commands: `/qoder.usage`, `/qoder.claim`., `/qoder.models`
36
+ - To use CN: run `/login qoder-cn` and use `/model qoder-cn/<id>`. Commands: `/qoder-cn.usage`, `/qoder-cn.claim`., `/qoder-cn.models`
36
37
 
37
38
  > Check with `pi list` and inspect `~/.pi/agent/extensions/`: a leftover
38
39
  > `pi-provider-qoder` extension directory registers the same provider ids.
@@ -161,6 +162,33 @@ Notes:
161
162
  - The command uses the same stored token as chat requests, refreshing it first,
162
163
  so it reports the same quota the model is actually billed against.
163
164
 
165
+ ### `/qoder-cn.models`
166
+
167
+ Refreshes the model catalog from `/algo/api/v2/model/list` and reports what moved:
168
+
169
+ ```text
170
+ Qoder CN Plan models · 11 offered by the catalog
171
+
172
+ New (4)
173
+ + kimi-k3.1-pro Kimi-K3.1-Pro · Qoder CN
174
+ + minimax-m3.1-flash MiniMax-M3.1-Flash · Qoder CN
175
+ Renamed upstream (1)
176
+ ~ deepseek-v4-pro DeepSeek V4 Pro · Qoder CN -> DeepSeek-V4-Pro · Qoder CN
177
+ Gone (4)
178
+ - kimi-k3 Kimi-K3 · Qoder CN
179
+ - minimax-m2.7 MiniMax M2.7 · Qoder CN
180
+ ```
181
+
182
+ The provider list is re-registered in the running session, so the change is usable
183
+ in `/model` immediately — no restart. Append `list` to print the models pi currently
184
+ has cached, without touching the network.
185
+
186
+ The catalog key is a slot, not a model: upstream has served Kimi-K2.6 through
187
+ Kimi-K3.1-Pro under `kmodel_latest`. Ids are therefore slugs of the display name, so
188
+ an upgrade arrives as a new id (`kimi-k3` -> `kimi-k3.1-pro`) and a model upstream
189
+ stops offering leaves the list. Retired ids still resolve for an existing
190
+ `settings.json` (they route to the key they always used) but are no longer advertised.
191
+
164
192
  ### `/qoder-cn.claim`
165
193
 
166
194
  Daily check-in command to claim the 100 free Credits reward on Qoder CN personal
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"];
@@ -2647,6 +2447,81 @@ async function runClaimCommand(mode, args, ctx) {
2647
2447
  }
2648
2448
  }
2649
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}`);
2460
+ }
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}`);
2464
+ }
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}`);
2468
+ }
2469
+ if (diff.added.length === 0 && diff.renamed.length === 0 && diff.removed.length === 0) {
2470
+ lines.push("", "No change since the last refresh.");
2471
+ }
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?.();
2488
+ }
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;
2502
+ }
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;
2509
+ }
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");
2522
+ }
2523
+ }
2524
+
2650
2525
  // src/commands/usage.ts
2651
2526
  init_region();
2652
2527
  var DEFAULT_GRANT_CREDITS = 100;
@@ -2655,7 +2530,7 @@ function positiveOr(value, fallback) {
2655
2530
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
2656
2531
  }
2657
2532
  function estimateRollingAddOnExpiry(addOn, grant, now = Date.now()) {
2658
- if (!addOn || !addOn.total || addOn.total <= 0 || !addOn.remaining || addOn.remaining <= 0) {
2533
+ if (!addOn?.total || addOn.total <= 0 || !addOn.remaining || addOn.remaining <= 0) {
2659
2534
  return null;
2660
2535
  }
2661
2536
  const shape = typeof grant === "number" ? { expiryMs: grant } : grant ?? {};
@@ -4158,13 +4033,16 @@ function registerQoderProvider(pi, mode) {
4158
4033
  streamSimple: streamQoder
4159
4034
  });
4160
4035
  }
4036
+ function reregisterQoderProvider(pi, mode) {
4037
+ registerQoderProvider(pi, mode);
4038
+ }
4161
4039
  async function refreshModelsAtStartup(mode) {
4162
4040
  const providerID = getQoderRegionConfig(mode).providerID;
4163
- if (!isCacheStale(mode)) return;
4041
+ if (!isCacheStale(mode)) return null;
4164
4042
  const credentials = getCachedCredentials("", providerID);
4165
- if (!credentials?.access) return;
4043
+ if (!credentials?.access) return null;
4166
4044
  const region = getQoderRegionConfig(mode);
4167
- await updateQoderModelsCache(
4045
+ return updateQoderModelsCache(
4168
4046
  credentials.access,
4169
4047
  credentials.userID || "qoder-user",
4170
4048
  credentials.name || region.userNameFallback,
@@ -4216,6 +4094,12 @@ function registerCommands(pi) {
4216
4094
  await runClaimCommand("cn", args, ctx);
4217
4095
  }
4218
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
+ });
4219
4103
  pi.registerCommand("qoder.usage", {
4220
4104
  description: "Show Qoder Global quota: plan + add-on credits and check-in status (append 'json' for raw payload)",
4221
4105
  handler: async (args, ctx) => {
@@ -4228,6 +4112,12 @@ function registerCommands(pi) {
4228
4112
  await runClaimCommand("global", args, ctx);
4229
4113
  }
4230
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
+ });
4231
4121
  }
4232
4122
  export {
4233
4123
  index_default as default
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-provider-qoder-cn",
3
- "version": "0.2.25",
3
+ "version": "0.2.26",
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",