coding-tool-x 3.7.7 → 3.7.9

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.
@@ -233,6 +233,9 @@ const MODEL_ALIASES = {
233
233
  };
234
234
 
235
235
  const TEST_TIMEOUT_MS = 10000; // 10 seconds per model test
236
+ const MODEL_LIST_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
237
+ const MODEL_LIST_FAILURE_TTL_MS = 5 * 60 * 1000;
238
+ const MODEL_LIST_AUTH_FAILURE_TTL_MS = 60 * 60 * 1000;
236
239
  const CLAUDE_CODE_BETA_HEADER = 'claude-code-20250219,interleaved-thinking-2025-05-14';
237
240
 
238
241
  const MODEL_UNAVAILABLE_HINTS = [
@@ -797,6 +800,84 @@ function isSignatureCacheValid(cacheEntry, signatureKey, expectedSignature) {
797
800
  return cacheEntry[signatureKey] === expectedSignature;
798
801
  }
799
802
 
803
+ function getCacheAgeMs(timestamp, now = Date.now()) {
804
+ const checkedAt = Date.parse(timestamp || '');
805
+ return Number.isFinite(checkedAt) ? Math.max(0, now - checkedAt) : Infinity;
806
+ }
807
+
808
+ function isCacheFresh(timestamp, ttlMs, now = Date.now()) {
809
+ return getCacheAgeMs(timestamp, now) < ttlMs;
810
+ }
811
+
812
+ function getModelListFailureTtl(statusCode) {
813
+ return [401, 403, 404, 429].includes(Number(statusCode))
814
+ ? MODEL_LIST_AUTH_FAILURE_TTL_MS
815
+ : MODEL_LIST_FAILURE_TTL_MS;
816
+ }
817
+
818
+ function buildCachedModelListResult(entry, listSignature, forceRefresh) {
819
+ if (forceRefresh || !isSignatureCacheValid(entry, 'listSignature', listSignature)) {
820
+ return null;
821
+ }
822
+
823
+ const failure = entry.modelListFailure;
824
+ if (failure && Date.parse(failure.retryAfter || '') > Date.now()) {
825
+ return {
826
+ models: Array.isArray(entry.fetchedModels) ? entry.fetchedModels : [],
827
+ supported: true,
828
+ cached: true,
829
+ stale: true,
830
+ backoff: true,
831
+ fallbackUsed: false,
832
+ error: failure.error || '模型列表暂时不可用',
833
+ errorHint: failure.errorHint || '请稍后重试,或手动填写模型名称',
834
+ statusCode: failure.statusCode,
835
+ lastChecked: entry.lastChecked || failure.failedAt || null,
836
+ retryAfter: failure.retryAfter
837
+ };
838
+ }
839
+
840
+ if (!Array.isArray(entry.fetchedModels)) {
841
+ return null;
842
+ }
843
+
844
+ const stale = !isCacheFresh(entry.lastChecked, MODEL_LIST_CACHE_TTL_MS);
845
+ const result = {
846
+ models: entry.fetchedModels,
847
+ supported: true,
848
+ cached: true,
849
+ fallbackUsed: false,
850
+ error: null,
851
+ lastChecked: entry.lastChecked || null
852
+ };
853
+ if (stale) {
854
+ result.stale = true;
855
+ result.errorHint = '模型目录缓存已过期;可点击“刷新可选模型”更新。';
856
+ }
857
+ return result;
858
+ }
859
+
860
+ function saveModelListFailure(cache, cacheKey, listSignature, result = {}) {
861
+ const now = new Date();
862
+ const ttlMs = getModelListFailureTtl(result.statusCode);
863
+ const previous = cache[cacheKey] || {};
864
+ const failure = {
865
+ error: result.error || '模型列表暂时不可用',
866
+ errorHint: result.errorHint || '请稍后重试,或手动填写模型名称',
867
+ statusCode: result.statusCode || null,
868
+ failedAt: now.toISOString(),
869
+ retryAfter: new Date(now.getTime() + ttlMs).toISOString()
870
+ };
871
+ cache[cacheKey] = {
872
+ ...previous,
873
+ fetchedModels: Array.isArray(previous.fetchedModels) ? previous.fetchedModels : [],
874
+ listSignature,
875
+ modelListFailure: failure
876
+ };
877
+ saveModelCache(cache);
878
+ return failure;
879
+ }
880
+
800
881
  /**
801
882
  * Test if a specific model is available for a channel
802
883
  * @param {Object} channel - Channel configuration
@@ -869,8 +950,10 @@ async function probeModelAvailability(channel, channelType, options = {}) {
869
950
  });
870
951
  const cacheKey = getChannelCacheKey(channel, probeSignature);
871
952
 
872
- // Return cached result if channel and probe options are unchanged
873
- if (!forceRefresh && isSignatureCacheValid(cache[cacheKey], 'probeSignature', probeSignature)) {
953
+ // Return cached result only while the matching probe result is still fresh.
954
+ if (!forceRefresh
955
+ && isSignatureCacheValid(cache[cacheKey], 'probeSignature', probeSignature)
956
+ && isCacheFresh(cache[cacheKey].lastChecked, MODEL_LIST_CACHE_TTL_MS)) {
874
957
  return {
875
958
  availableModels: cache[cacheKey].availableModels || [],
876
959
  preferredTestModel: cache[cacheKey].preferredTestModel || null,
@@ -1026,21 +1109,17 @@ async function fetchModelsFromProvider(channel, channelType, options = {}) {
1026
1109
  });
1027
1110
  const cacheKey = getChannelCacheKey(channel, listSignature);
1028
1111
 
1029
- // Check cache first, and only reuse when channel/list context is unchanged
1030
- if (!forceRefresh
1031
- && isSignatureCacheValid(cache[cacheKey], 'listSignature', listSignature)
1032
- && Array.isArray(cache[cacheKey].fetchedModels)) {
1033
- return {
1034
- models: cache[cacheKey].fetchedModels || [],
1035
- supported: true,
1036
- cached: true,
1037
- fallbackUsed: false,
1038
- error: null,
1039
- lastChecked: cache[cacheKey].lastChecked
1040
- };
1041
- }
1112
+ // Reuse a fresh catalog immediately. Expired catalogs are also returned as
1113
+ // stale data so opening the editor never blocks on the provider; the user
1114
+ // can explicitly request a refresh.
1115
+ const cachedResult = buildCachedModelListResult(cache[cacheKey], listSignature, forceRefresh);
1116
+ if (cachedResult) return cachedResult;
1042
1117
 
1043
1118
  return new Promise((resolve) => {
1119
+ const resolveFailure = (result) => {
1120
+ saveModelListFailure(cache, cacheKey, listSignature, result);
1121
+ resolve(result);
1122
+ };
1044
1123
  try {
1045
1124
  const baseUrl = channel.baseUrl.trim().replace(/\/+$/, '');
1046
1125
  const endpoint = capability.modelListEndpoint; // e.g. '/v1/models'
@@ -1095,7 +1174,8 @@ async function fetchModelsFromProvider(channel, channelType, options = {}) {
1095
1174
  availableModels: cache[cacheKey]?.availableModels || [],
1096
1175
  preferredTestModel: cache[cacheKey]?.preferredTestModel || null,
1097
1176
  probeSignature: cache[cacheKey]?.probeSignature || null,
1098
- listSignature
1177
+ listSignature,
1178
+ modelListFailure: null
1099
1179
  };
1100
1180
 
1101
1181
  cache[cacheKey] = cacheEntry;
@@ -1113,7 +1193,7 @@ async function fetchModelsFromProvider(channel, channelType, options = {}) {
1113
1193
  });
1114
1194
  } catch (parseError) {
1115
1195
  console.error(`[ModelDetector] Failed to parse models response: ${parseError.message}`);
1116
- resolve({
1196
+ resolveFailure({
1117
1197
  models: [],
1118
1198
  supported: true,
1119
1199
  cached: false,
@@ -1133,7 +1213,7 @@ async function fetchModelsFromProvider(channel, channelType, options = {}) {
1133
1213
  errorMessage = 'Cloudflare 防护拦截,无法自动获取模型列表';
1134
1214
  errorHint = '该 API 端点受 Cloudflare 保护,请手动填写模型名称';
1135
1215
  console.warn(`[ModelDetector] Cloudflare protection detected for ${channel.name}, no fallback models injected`);
1136
- resolve({
1216
+ resolveFailure({
1137
1217
  models: [],
1138
1218
  supported: false,
1139
1219
  cached: false,
@@ -1146,7 +1226,7 @@ async function fetchModelsFromProvider(channel, channelType, options = {}) {
1146
1226
  errorMessage = 'API 密钥认证失败';
1147
1227
  errorHint = '请检查 API 密钥是否正确配置';
1148
1228
  console.error(`[ModelDetector] Authentication failed for ${channel.name}: ${res.statusCode} - ${errorMessage}`);
1149
- resolve({
1229
+ resolveFailure({
1150
1230
  models: [],
1151
1231
  supported: true,
1152
1232
  cached: false,
@@ -1159,7 +1239,7 @@ async function fetchModelsFromProvider(channel, channelType, options = {}) {
1159
1239
  errorMessage = '访问被拒绝';
1160
1240
  errorHint = '请检查 API 密钥权限或联系服务提供商';
1161
1241
  console.error(`[ModelDetector] Access denied for ${channel.name}: ${res.statusCode} - ${errorMessage}`);
1162
- resolve({
1242
+ resolveFailure({
1163
1243
  models: [],
1164
1244
  supported: true,
1165
1245
  cached: false,
@@ -1171,7 +1251,7 @@ async function fetchModelsFromProvider(channel, channelType, options = {}) {
1171
1251
  }
1172
1252
  } else if (res.statusCode === 404) {
1173
1253
  console.warn(`[ModelDetector] Model list endpoint not found for ${channel.name}`);
1174
- resolve({
1254
+ resolveFailure({
1175
1255
  models: [],
1176
1256
  supported: false,
1177
1257
  cached: false,
@@ -1182,7 +1262,7 @@ async function fetchModelsFromProvider(channel, channelType, options = {}) {
1182
1262
  });
1183
1263
  } else if (res.statusCode === 429) {
1184
1264
  console.warn(`[ModelDetector] Rate limited for ${channel.name}`);
1185
- resolve({
1265
+ resolveFailure({
1186
1266
  models: [],
1187
1267
  supported: true,
1188
1268
  cached: false,
@@ -1206,7 +1286,7 @@ async function fetchModelsFromProvider(channel, channelType, options = {}) {
1206
1286
  })
1207
1287
  .catch((error) => {
1208
1288
  console.error(`[ModelDetector] Failed to read models response: ${error.message}`);
1209
- resolve({
1289
+ resolveFailure({
1210
1290
  models: [],
1211
1291
  supported: true,
1212
1292
  cached: false,
@@ -1218,7 +1298,7 @@ async function fetchModelsFromProvider(channel, channelType, options = {}) {
1218
1298
 
1219
1299
  req.on('error', (error) => {
1220
1300
  console.error(`[ModelDetector] Network error fetching models from ${channel.name}: ${error.message}`);
1221
- resolve({
1301
+ resolveFailure({
1222
1302
  models: [],
1223
1303
  supported: true,
1224
1304
  cached: false,
@@ -1230,7 +1310,7 @@ async function fetchModelsFromProvider(channel, channelType, options = {}) {
1230
1310
  req.on('timeout', () => {
1231
1311
  req.destroy();
1232
1312
  console.error(`[ModelDetector] Timeout fetching models from ${channel.name}`);
1233
- resolve({
1313
+ resolveFailure({
1234
1314
  models: [],
1235
1315
  supported: true,
1236
1316
  cached: false,
@@ -1243,7 +1323,7 @@ async function fetchModelsFromProvider(channel, channelType, options = {}) {
1243
1323
 
1244
1324
  } catch (error) {
1245
1325
  console.error(`[ModelDetector] Error in fetchModelsFromProvider: ${error.message}`);
1246
- resolve({
1326
+ resolveFailure({
1247
1327
  models: [],
1248
1328
  supported: true,
1249
1329
  cached: false,
@@ -21,7 +21,7 @@ const {
21
21
  upsertSyncedChannels
22
22
  } = require('./channel-sync-utils');
23
23
 
24
- const OMP_THINKING_SUFFIX_RE = /:(minimal|low|medium|high|xhigh)$/;
24
+ const OMP_THINKING_SUFFIX_RE = /:(minimal|low|medium|high|xhigh|off)$/;
25
25
 
26
26
  class OmpChannelService extends BaseChannelService {
27
27
  constructor() {
@@ -44,6 +44,14 @@ class OmpChannelService extends BaseChannelService {
44
44
  }
45
45
  normalized.model = normalized.model || null;
46
46
  normalized.models = Array.isArray(normalized.models) ? normalized.models : [];
47
+ normalized.modelMetadataMode = ['auto', 'hybrid', 'manual'].includes(normalized.modelMetadataMode)
48
+ ? normalized.modelMetadataMode
49
+ : 'auto';
50
+ normalized.modelBindings = Array.isArray(normalized.modelBindings) ? normalized.modelBindings : [];
51
+ normalized.providerConfig = normalized.providerConfig && typeof normalized.providerConfig === 'object'
52
+ && !Array.isArray(normalized.providerConfig)
53
+ ? normalized.providerConfig
54
+ : {};
47
55
  normalized.allowedModels = Array.isArray(normalized.allowedModels) ? normalized.allowedModels : [];
48
56
  normalized.speedTestModel = normalized.speedTestModel || null;
49
57
  normalized.modelRedirects = Array.isArray(normalized.modelRedirects) ? normalized.modelRedirects : [];
@@ -229,6 +229,28 @@ function readTextFile(filePath, fallback = '') {
229
229
  }
230
230
 
231
231
  function getOmpStatus(env = process.env, options = {}) {
232
+ if (options.resolveRuntime === false) {
233
+ const paths = getOmpPaths(env, options);
234
+ return {
235
+ installed: fs.existsSync(paths.agentDir),
236
+ runtime: 'omp',
237
+ command: getOmpCommand(env),
238
+ commandSource: 'not-probed',
239
+ agentDir: paths.agentDir,
240
+ settingsPath: paths.settings,
241
+ authPath: paths.auth,
242
+ modelsPath: paths.modelsYml,
243
+ modelsYmlPath: paths.modelsYml,
244
+ modelsJsonLegacyPath: paths.modelsJsonLegacy,
245
+ sessionsDir: paths.sessions,
246
+ skillsDir: paths.skills,
247
+ promptsDir: paths.prompts,
248
+ commandsDir: paths.commands,
249
+ extensionsDir: paths.extensions,
250
+ themesDir: paths.themes,
251
+ packagesDir: paths.packages
252
+ };
253
+ }
232
254
  const runtime = resolveOmpRuntime(env, options);
233
255
  const paths = getOmpPaths(env, { ...options, runtime });
234
256
  return {