coding-tool-x 3.7.9 → 3.7.11

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 (33) hide show
  1. package/dist/web/assets/{Analytics-D14GGF2y.js → Analytics-Ck_00Brj.js} +1 -1
  2. package/dist/web/assets/{ConfigTemplates-C0TeUSZg.js → ConfigTemplates-Cpw2vUa8.js} +1 -1
  3. package/dist/web/assets/Home-Bft3Kztl.js +1 -0
  4. package/dist/web/assets/Home-CHZJkkjH.css +1 -0
  5. package/dist/web/assets/{PluginManager-DPZvPrT4.js → PluginManager-2RlvEJCT.js} +1 -1
  6. package/dist/web/assets/{ProjectList-DUaiCvsF.js → ProjectList-_ecCngpl.js} +1 -1
  7. package/dist/web/assets/{SessionList-XdHNM61o.js → SessionList-DlWcw3hO.js} +1 -1
  8. package/dist/web/assets/{SkillManager-I4LFlsVN.js → SkillManager-CnY7IXkX.js} +1 -1
  9. package/dist/web/assets/{WorkspaceManager-zMI7Lhzf.js → WorkspaceManager-3mqghi09.js} +1 -1
  10. package/dist/web/assets/{index-C5nLTbOk.css → index-BceXgSSe.css} +1 -1
  11. package/dist/web/assets/index-k5oP7c-A.js +2 -0
  12. package/dist/web/index.html +2 -2
  13. package/package.json +1 -1
  14. package/src/commands/toggle-proxy.js +7 -8
  15. package/src/commands/ui.js +11 -1
  16. package/src/config/default.js +1 -1
  17. package/src/index.js +41 -15
  18. package/src/server/api/omp-channels.js +16 -5
  19. package/src/server/api/omp-config.js +1 -1
  20. package/src/server/api/omp-proxy.js +13 -42
  21. package/src/server/index.js +3 -3
  22. package/src/server/omp-proxy-server.js +129 -10
  23. package/src/server/services/channel-scheduler.js +59 -9
  24. package/src/server/services/omp-auth-gateway-client.js +62 -0
  25. package/src/server/services/omp-channels.js +109 -12
  26. package/src/server/services/omp-gateway-routing.js +295 -0
  27. package/src/server/services/omp-gateway.js +919 -0
  28. package/src/server/services/omp-session-log-observer.js +138 -0
  29. package/src/server/services/omp-sessions.js +80 -0
  30. package/src/server/services/omp-settings-manager.js +358 -89
  31. package/dist/web/assets/Home-CUh_BsGm.js +0 -1
  32. package/dist/web/assets/Home-CqnBXFxj.css +0 -1
  33. package/dist/web/assets/index-CCP7sRMP.js +0 -2
@@ -3,6 +3,7 @@ const path = require('path');
3
3
  const { spawnSync } = require('child_process');
4
4
  const yaml = require('js-yaml');
5
5
  const ompConfig = require('./omp-config');
6
+ const { prepareManagedOmpChannels } = require('./omp-gateway-routing');
6
7
  const { MODEL_METADATA, MODEL_ALIASES } = require('../../config/model-metadata');
7
8
  const {
8
9
  MODEL_METADATA_MODES,
@@ -15,7 +16,7 @@ const {
15
16
 
16
17
  const MANAGED_PROVIDER_PREFIX = 'ctx-';
17
18
  const MODELS_BACKUP_MARKER = '.ctx-backup-';
18
- const VISIBILITY_STATE_VERSION = 1;
19
+ const VISIBILITY_STATE_VERSION = 2;
19
20
  const MANAGED_VISIBILITY_STATE_FILE = 'coding-tool-x-omp-managed-visibility.json';
20
21
  const NO_MANAGED_MODELS_SELECTOR = `${MANAGED_PROVIDER_PREFIX}coding-tool-x/__no_models_configured__`;
21
22
  let lastManagedOmpSyncResult = null;
@@ -45,6 +46,12 @@ function normalizeProviderId(value = '') {
45
46
  }
46
47
 
47
48
  function getManagedProviderId(channel = {}) {
49
+ const explicitId = normalizeProviderId(channel.managedProviderId || '');
50
+ if (channel.managedProviderId) {
51
+ return explicitId.startsWith(MANAGED_PROVIDER_PREFIX)
52
+ ? explicitId
53
+ : `${MANAGED_PROVIDER_PREFIX}${explicitId}`;
54
+ }
48
55
  const baseId = normalizeProviderId(channel.providerKey || channel.provider || channel.name || channel.id);
49
56
  return baseId.startsWith(MANAGED_PROVIDER_PREFIX) ? baseId : `${MANAGED_PROVIDER_PREFIX}${baseId}`;
50
57
  }
@@ -116,6 +123,49 @@ function getOwnProperty(object, key) {
116
123
  : false;
117
124
  }
118
125
 
126
+ function cloneJson(value) {
127
+ if (value === undefined) return undefined;
128
+ return JSON.parse(JSON.stringify(value));
129
+ }
130
+
131
+ function captureFileSnapshots(filePaths = []) {
132
+ return filePaths.map((filePath) => ({
133
+ filePath,
134
+ existed: Boolean(filePath && fs.existsSync(filePath)),
135
+ content: filePath && fs.existsSync(filePath) ? fs.readFileSync(filePath) : null
136
+ }));
137
+ }
138
+
139
+ function restoreFileSnapshots(snapshots = []) {
140
+ snapshots.forEach((snapshot) => {
141
+ if (!snapshot.filePath) return;
142
+ if (snapshot.existed) {
143
+ ensureOmpDir(path.dirname(snapshot.filePath));
144
+ writeFileAtomic(snapshot.filePath, snapshot.content);
145
+ } else if (fs.existsSync(snapshot.filePath)) {
146
+ fs.unlinkSync(snapshot.filePath);
147
+ }
148
+ });
149
+ }
150
+
151
+ function writeFileAtomic(filePath, content) {
152
+ ensureOmpDir(path.dirname(filePath));
153
+ const temporaryPath = `${filePath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
154
+ const mode = fs.existsSync(filePath)
155
+ ? fs.statSync(filePath).mode & 0o777
156
+ : 0o600;
157
+ try {
158
+ fs.writeFileSync(temporaryPath, content, { mode });
159
+ fs.chmodSync(temporaryPath, mode);
160
+ fs.renameSync(temporaryPath, filePath);
161
+ } catch (error) {
162
+ if (fs.existsSync(temporaryPath)) {
163
+ fs.unlinkSync(temporaryPath);
164
+ }
165
+ throw error;
166
+ }
167
+ }
168
+
119
169
  function normalizeModels(channel = {}, options = {}) {
120
170
  const models = [];
121
171
  const catalogIndex = options.catalogIndex instanceof Map ? options.catalogIndex : new Map();
@@ -170,7 +220,10 @@ function normalizeModels(channel = {}, options = {}) {
170
220
  }
171
221
 
172
222
  function readOmpCatalogModels(command, providerId, options = {}) {
173
- const cacheKey = `${command}\u0000${providerId}`;
223
+ const requestedModelIds = uniqueStrings(options.requestedModelIds);
224
+ const requested = new Set(requestedModelIds.map(modelId => modelId.toLowerCase()));
225
+ const useFullCatalog = requested.size > 0;
226
+ const cacheKey = `${command}\u0000${useFullCatalog ? `requested:${providerId}:${[...requested].sort().join(',')}` : providerId}`;
174
227
  const cached = ompCatalogCache.get(cacheKey);
175
228
  const now = Date.now();
176
229
  const cacheTtl = cached?.failed ? OMP_CATALOG_FAILURE_CACHE_TTL_MS : OMP_CATALOG_CACHE_TTL_MS;
@@ -178,7 +231,8 @@ function readOmpCatalogModels(command, providerId, options = {}) {
178
231
  return cached.models.map(model => ({ ...model }));
179
232
  }
180
233
  const runner = options.catalogRunner || options.modelsRunner || spawnSync;
181
- const result = runner(command, ['models', providerId, '--json'], {
234
+ const args = useFullCatalog ? ['models', '--json'] : ['models', providerId, '--json'];
235
+ const result = runner(command, args, {
182
236
  encoding: 'utf8',
183
237
  env: {
184
238
  ...process.env,
@@ -194,7 +248,22 @@ function readOmpCatalogModels(command, providerId, options = {}) {
194
248
  }
195
249
  try {
196
250
  const parsed = JSON.parse(normalizeCommandOutput(result?.stdout).trim() || '{}');
197
- const models = Array.isArray(parsed.models) ? parsed.models : [];
251
+ const rawModels = Array.isArray(parsed.models) ? parsed.models : [];
252
+ let models = rawModels;
253
+ if (useFullCatalog) {
254
+ const providerCandidates = new Set([providerId, `${MANAGED_PROVIDER_PREFIX}${providerId}`]);
255
+ const selected = new Map();
256
+ for (const model of rawModels) {
257
+ const id = String(model?.id || '').trim().toLowerCase();
258
+ if (!requested.has(id)) continue;
259
+ const provider = normalizeProviderId(model?.provider || '');
260
+ const previous = selected.get(id);
261
+ if (!previous || (providerCandidates.has(provider) && !providerCandidates.has(normalizeProviderId(previous.provider || '')))) {
262
+ selected.set(id, model);
263
+ }
264
+ }
265
+ models = [...selected.values()];
266
+ }
198
267
  ompCatalogCache.set(cacheKey, { cachedAt: now, models, failed: false });
199
268
  return models.map(model => ({ ...model }));
200
269
  } catch {
@@ -203,6 +272,7 @@ function readOmpCatalogModels(command, providerId, options = {}) {
203
272
  }
204
273
  }
205
274
 
275
+
206
276
  function getOmpCatalogModels(providerId, options = {}) {
207
277
  const normalizedProviderId = normalizeProviderId(providerId);
208
278
  if (!normalizedProviderId) return [];
@@ -257,13 +327,32 @@ function buildProviderEntry(channel = {}, options = {}) {
257
327
  const providerName = channel.providerKey || channel.provider || channel.name || channel.id || 'provider';
258
328
  options.warnings.push(...normalizedProvider.warnings.map(message => `${providerName}: ${message}`));
259
329
  }
330
+ let models = normalizeModels(channel, { catalogIndex, warnings: options.warnings });
331
+ if (channel._ompGatewayRoute) {
332
+ models = models.map((model) => {
333
+ const sanitized = { ...model };
334
+ delete sanitized.baseUrl;
335
+ delete sanitized.headers;
336
+ return sanitized;
337
+ });
338
+ }
260
339
  const entry = compactObject({
261
340
  ...normalizedProvider.config,
262
341
  baseUrl: String(channel.baseUrl || providerConfig.baseUrl || '').trim(),
263
342
  api: normalizeProviderApi(channel.providerApi || channel.api || channel.wireApi || providerConfig.api),
264
- models: normalizeModels(channel, { catalogIndex, warnings: options.warnings })
343
+ models
265
344
  });
266
345
 
346
+ if (channel._ompGatewayRoute) {
347
+ delete entry.headers;
348
+ delete entry.apiKey;
349
+ if (channel.authMode === 'none') {
350
+ entry.auth = 'none';
351
+ } else {
352
+ delete entry.auth;
353
+ }
354
+ }
355
+
267
356
  const apiKey = String(channel.apiKey || providerConfig.apiKey || '').trim();
268
357
  if (apiKey) {
269
358
  entry.apiKey = apiKey;
@@ -301,7 +390,7 @@ function writeModelsConfig(config, filePath = getOmpPaths().modelsYml) {
301
390
  noRefs: true,
302
391
  sortKeys: false
303
392
  });
304
- fs.writeFileSync(filePath, doc, 'utf8');
393
+ writeFileAtomic(filePath, doc);
305
394
  }
306
395
 
307
396
  function getOmpSettingsPath(paths = getOmpPaths()) {
@@ -339,7 +428,7 @@ function writeOmpSettingsConfig(config, filePath = getOmpSettingsPath()) {
339
428
  noRefs: true,
340
429
  sortKeys: false
341
430
  });
342
- fs.writeFileSync(filePath, doc, 'utf8');
431
+ writeFileAtomic(filePath, doc);
343
432
  }
344
433
 
345
434
  function getModelsBackupPrefix(filePath) {
@@ -400,7 +489,7 @@ function readManagedVisibilityState(paths = getOmpPaths()) {
400
489
  function writeManagedVisibilityState(state, paths = getOmpPaths()) {
401
490
  const filePath = getManagedVisibilityStatePath(paths);
402
491
  ensureOmpDir(path.dirname(filePath));
403
- fs.writeFileSync(filePath, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
492
+ writeFileAtomic(filePath, `${JSON.stringify(state, null, 2)}\n`);
404
493
  return filePath;
405
494
  }
406
495
 
@@ -582,6 +671,9 @@ function buildManagedModelsConfig(channels = [], baseConfig = readModelsConfig()
582
671
  }
583
672
 
584
673
  function getOriginalProviderId(channel = {}) {
674
+ if (channel.originalProviderId) {
675
+ return normalizeProviderId(channel.originalProviderId);
676
+ }
585
677
  const providerId = normalizeProviderId(channel.providerKey || channel.provider || channel.name || channel.id);
586
678
  return isManagedProviderId(providerId)
587
679
  ? providerId.slice(MANAGED_PROVIDER_PREFIX.length)
@@ -659,11 +751,18 @@ function captureOriginalVisibility(settings = {}) {
659
751
  const modelRoles = settings.modelRoles && typeof settings.modelRoles === 'object' && !Array.isArray(settings.modelRoles)
660
752
  ? settings.modelRoles
661
753
  : {};
754
+ const fallbackChains = settings.retry && typeof settings.retry === 'object' && !Array.isArray(settings.retry)
755
+ ? settings.retry.fallbackChains
756
+ : undefined;
662
757
  return {
663
758
  enabledModels: getOwnProperty(settings, 'enabledModels') ? uniqueStrings(settings.enabledModels) : null,
664
759
  disabledProviders: getOwnProperty(settings, 'disabledProviders') ? uniqueStrings(settings.disabledProviders) : null,
665
760
  modelRolesHadDefault: getOwnProperty(modelRoles, 'default'),
666
- modelRolesDefault: getOwnProperty(modelRoles, 'default') ? String(modelRoles.default || '') : null
761
+ modelRolesDefault: getOwnProperty(modelRoles, 'default') ? String(modelRoles.default || '') : null,
762
+ modelRolesHadValue: getOwnProperty(settings, 'modelRoles'),
763
+ modelRoles: getOwnProperty(settings, 'modelRoles') ? cloneJson(modelRoles) : null,
764
+ retryFallbackChainsHadValue: fallbackChains !== undefined,
765
+ retryFallbackChains: fallbackChains !== undefined ? cloneJson(fallbackChains) : null
667
766
  };
668
767
  }
669
768
 
@@ -681,22 +780,86 @@ function collectUserDisabledProviders(settings = {}, previousState = null) {
681
780
  return uniqueStrings([...originalDisabled, ...userAddedDuringManagedMode]);
682
781
  }
683
782
 
684
- function applyManagedModelRole(settings, visibility, previousState = null) {
685
- const roles = settings.modelRoles && typeof settings.modelRoles === 'object' && !Array.isArray(settings.modelRoles)
686
- ? { ...settings.modelRoles }
687
- : {};
688
- const currentDefault = String(roles.default || '').trim();
689
- const previousManagedDefault = String(previousState?.managedDefaultModel || '').trim();
690
- if (!visibility.managedDefaultModel) {
691
- return roles;
783
+ function buildProviderMappings(channels = []) {
784
+ const originalToManaged = {};
785
+ const managedToOriginal = {};
786
+ channels.forEach((channel) => {
787
+ const original = getOriginalProviderId(channel);
788
+ const managed = getManagedProviderId(channel);
789
+ if (!original || !managed) return;
790
+ if (!originalToManaged[original]) {
791
+ originalToManaged[original] = managed;
792
+ }
793
+ managedToOriginal[managed] = original;
794
+ });
795
+ return { originalToManaged, managedToOriginal };
796
+ }
797
+
798
+ function rewriteModelSelector(value, providerMap = {}) {
799
+ if (typeof value !== 'string') return value;
800
+ const index = value.indexOf('/');
801
+ if (index <= 0) return value;
802
+ const providerId = value.slice(0, index);
803
+ const replacement = providerMap[providerId];
804
+ return replacement ? `${replacement}${value.slice(index)}` : value;
805
+ }
806
+
807
+ function rewriteSelectorTree(value, providerMap = {}) {
808
+ if (typeof value === 'string') return rewriteModelSelector(value, providerMap);
809
+ if (Array.isArray(value)) return value.map(item => rewriteSelectorTree(item, providerMap));
810
+ if (value && typeof value === 'object') {
811
+ return Object.fromEntries(
812
+ Object.entries(value).map(([key, item]) => [key, rewriteSelectorTree(item, providerMap)])
813
+ );
814
+ }
815
+ return value;
816
+ }
817
+
818
+ function removeManagedSelectorTree(value) {
819
+ if (typeof value === 'string') {
820
+ const providerId = String(value).split('/')[0];
821
+ return isManagedProviderId(providerId) ? undefined : value;
822
+ }
823
+ if (Array.isArray(value)) {
824
+ return value
825
+ .map(removeManagedSelectorTree)
826
+ .filter(item => item !== undefined);
827
+ }
828
+ if (value && typeof value === 'object') {
829
+ return Object.fromEntries(
830
+ Object.entries(value)
831
+ .map(([key, item]) => [key, removeManagedSelectorTree(item)])
832
+ .filter(([, item]) => item !== undefined)
833
+ );
692
834
  }
693
- const defaultIsManaged = visibility.managedEnabledModels.includes(currentDefault);
694
- if (!currentDefault || !defaultIsManaged || currentDefault === previousManagedDefault) {
835
+ return value;
836
+ }
837
+
838
+ function applyManagedModelRoles(settings, visibility, mappings, previousState = null) {
839
+ const originalRoles = previousState?.active && previousState.original?.modelRolesHadValue
840
+ ? previousState.original.modelRoles
841
+ : settings.modelRoles;
842
+ const roles = originalRoles && typeof originalRoles === 'object' && !Array.isArray(originalRoles)
843
+ ? rewriteSelectorTree(cloneJson(originalRoles), mappings.originalToManaged)
844
+ : {};
845
+ const originalDefault = String(originalRoles?.default || '');
846
+ const mappedDefault = rewriteModelSelector(originalDefault, mappings.originalToManaged);
847
+ const defaultWasMapped = originalDefault && mappedDefault !== originalDefault;
848
+ const defaultIsManaged = isManagedProviderId(String(roles.default || '').split('/')[0]);
849
+ if (visibility.managedDefaultModel && (!roles.default || (!defaultWasMapped && !defaultIsManaged))) {
695
850
  roles.default = visibility.managedDefaultModel;
696
851
  }
697
852
  return roles;
698
853
  }
699
854
 
855
+ function applyManagedFallbackChains(settings, mappings, previousState = null) {
856
+ const source = previousState?.active && previousState.original?.retryFallbackChainsHadValue
857
+ ? previousState.original.retryFallbackChains
858
+ : settings.retry?.fallbackChains;
859
+ if (source === undefined) return undefined;
860
+ return rewriteSelectorTree(cloneJson(source), mappings.originalToManaged);
861
+ }
862
+
700
863
  function syncManagedOmpVisibility(channels = [], modelsConfig = { providers: {} }, options = {}) {
701
864
  const paths = getOmpPaths();
702
865
  const settingsPath = getOmpSettingsPath(paths);
@@ -704,6 +867,7 @@ function syncManagedOmpVisibility(channels = [], modelsConfig = { providers: {}
704
867
  const settings = readOmpSettingsConfig(settingsPath);
705
868
  const previousState = readManagedVisibilityState(paths);
706
869
  const visibility = collectManagedVisibility(channels, modelsConfig, options);
870
+ const mappings = buildProviderMappings(channels);
707
871
  const original = previousState?.active && previousState.original
708
872
  ? previousState.original
709
873
  : captureOriginalVisibility(settings);
@@ -715,7 +879,13 @@ function syncManagedOmpVisibility(channels = [], modelsConfig = { providers: {}
715
879
  ...collectUserDisabledProviders(settings, previousState),
716
880
  ...visibility.managedDisabledProviders
717
881
  ]);
718
- settings.modelRoles = applyManagedModelRole(settings, visibility, previousState);
882
+ settings.modelRoles = applyManagedModelRoles(settings, visibility, mappings, previousState);
883
+ const fallbackChains = applyManagedFallbackChains(settings, mappings, previousState);
884
+ if (fallbackChains !== undefined) {
885
+ settings.retry = settings.retry && typeof settings.retry === 'object' && !Array.isArray(settings.retry)
886
+ ? { ...settings.retry, fallbackChains }
887
+ : { fallbackChains };
888
+ }
719
889
 
720
890
  const after = JSON.stringify(settings);
721
891
  if (before !== after) {
@@ -730,7 +900,12 @@ function syncManagedOmpVisibility(channels = [], modelsConfig = { providers: {}
730
900
  managedEnabledModelsForSettings: visibility.managedEnabledModelsForSettings,
731
901
  managedDisabledProviders: visibility.managedDisabledProviders,
732
902
  managedDefaultModel: visibility.managedDefaultModel,
903
+ providerMappings: mappings,
733
904
  original,
905
+ applied: {
906
+ modelRoles: cloneJson(settings.modelRoles),
907
+ retryFallbackChains: cloneJson(settings.retry?.fallbackChains)
908
+ },
734
909
  updatedAt: new Date().toISOString()
735
910
  };
736
911
  writeManagedVisibilityState(state, paths);
@@ -799,7 +974,39 @@ function removeManagedOmpVisibility(options = {}) {
799
974
  warnings.push('OMP disabledProviders was changed while coding-tool-x managed visibility was active; removed only managed provider entries.');
800
975
  }
801
976
 
802
- if (settings.modelRoles && typeof settings.modelRoles === 'object' && !Array.isArray(settings.modelRoles)) {
977
+ if (state.version >= 2 && state.original) {
978
+ const currentRoles = settings.modelRoles && typeof settings.modelRoles === 'object' && !Array.isArray(settings.modelRoles)
979
+ ? settings.modelRoles
980
+ : undefined;
981
+ if (JSON.stringify(currentRoles) === JSON.stringify(state.applied?.modelRoles)) {
982
+ if (state.original.modelRolesHadValue) {
983
+ settings.modelRoles = cloneJson(state.original.modelRoles);
984
+ } else {
985
+ delete settings.modelRoles;
986
+ }
987
+ } else if (currentRoles) {
988
+ settings.modelRoles = rewriteSelectorTree(currentRoles, state.providerMappings?.managedToOriginal || {});
989
+ warnings.push('OMP modelRoles was changed while coding-tool-x managed visibility was active; restored managed provider references only.');
990
+ }
991
+
992
+ const currentFallback = settings.retry?.fallbackChains;
993
+ if (JSON.stringify(currentFallback) === JSON.stringify(state.applied?.retryFallbackChains)) {
994
+ if (state.original.retryFallbackChainsHadValue) {
995
+ settings.retry = settings.retry && typeof settings.retry === 'object' && !Array.isArray(settings.retry)
996
+ ? { ...settings.retry, fallbackChains: cloneJson(state.original.retryFallbackChains) }
997
+ : { fallbackChains: cloneJson(state.original.retryFallbackChains) };
998
+ } else if (settings.retry && typeof settings.retry === 'object' && !Array.isArray(settings.retry)) {
999
+ delete settings.retry.fallbackChains;
1000
+ if (Object.keys(settings.retry).length === 0) delete settings.retry;
1001
+ }
1002
+ } else if (currentFallback !== undefined) {
1003
+ settings.retry.fallbackChains = rewriteSelectorTree(
1004
+ currentFallback,
1005
+ state.providerMappings?.managedToOriginal || {}
1006
+ );
1007
+ warnings.push('OMP retry.fallbackChains was changed while coding-tool-x managed visibility was active; restored managed provider references only.');
1008
+ }
1009
+ } else if (settings.modelRoles && typeof settings.modelRoles === 'object' && !Array.isArray(settings.modelRoles)) {
803
1010
  const roles = { ...settings.modelRoles };
804
1011
  if (roles.default === state.managedDefaultModel) {
805
1012
  if (state.original?.modelRolesHadDefault) {
@@ -825,6 +1032,39 @@ function removeManagedOmpVisibility(options = {}) {
825
1032
  }
826
1033
  warnings.push('Removed stale coding-tool-x managed OMP enabledModels entries without a visibility state file.');
827
1034
  }
1035
+
1036
+ if (settings.modelRoles && typeof settings.modelRoles === 'object' && !Array.isArray(settings.modelRoles)) {
1037
+ const roles = Object.fromEntries(
1038
+ Object.entries(settings.modelRoles).filter(([, model]) => {
1039
+ const providerId = String(model || '').split('/')[0];
1040
+ return !isManagedProviderId(providerId);
1041
+ })
1042
+ );
1043
+ if (Object.keys(roles).length !== Object.keys(settings.modelRoles).length) {
1044
+ settingsBackupPath = settingsBackupPath || createModelsBackupIfNeeded(settingsPath, options);
1045
+ if (Object.keys(roles).length > 0) {
1046
+ settings.modelRoles = roles;
1047
+ } else {
1048
+ delete settings.modelRoles;
1049
+ }
1050
+ warnings.push('Removed stale coding-tool-x managed OMP modelRoles entries without a visibility state file.');
1051
+ }
1052
+ }
1053
+
1054
+ const currentFallback = settings.retry?.fallbackChains;
1055
+ if (currentFallback !== undefined) {
1056
+ const fallbackChains = removeManagedSelectorTree(currentFallback);
1057
+ if (JSON.stringify(fallbackChains) !== JSON.stringify(currentFallback)) {
1058
+ settingsBackupPath = settingsBackupPath || createModelsBackupIfNeeded(settingsPath, options);
1059
+ if (fallbackChains && typeof fallbackChains === 'object' && Object.keys(fallbackChains).length > 0) {
1060
+ settings.retry.fallbackChains = fallbackChains;
1061
+ } else {
1062
+ delete settings.retry.fallbackChains;
1063
+ if (Object.keys(settings.retry).length === 0) delete settings.retry;
1064
+ }
1065
+ warnings.push('Removed stale coding-tool-x managed OMP retry.fallbackChains entries without a visibility state file.');
1066
+ }
1067
+ }
828
1068
  }
829
1069
 
830
1070
  const after = JSON.stringify(settings);
@@ -845,81 +1085,110 @@ function removeManagedOmpVisibility(options = {}) {
845
1085
 
846
1086
  function writeManagedOmpProviders(channels = [], options = {}) {
847
1087
  const paths = getOmpPaths();
1088
+ const settingsPath = getOmpSettingsPath(paths);
1089
+ const statePath = getManagedVisibilityStatePath(paths);
1090
+ const snapshots = captureFileSnapshots([paths.modelsYml, settingsPath, statePath]);
848
1091
  const buildWarnings = [];
849
- const config = buildManagedModelsConfig(channels, readModelsConfig(paths.modelsYml), {
850
- ...options,
851
- warnings: buildWarnings
852
- });
853
- const backupPath = createModelsBackupIfNeeded(paths.modelsYml, options);
854
- writeModelsConfig(config, paths.modelsYml);
855
- const visibility = syncManagedOmpVisibility(channels, config, options);
856
- const validation = options.validateWithCli === true
857
- ? validateOmpModelsConfig(options)
858
- : { skipped: true, reason: 'cli-validation-disabled', warnings: [] };
859
- const warnings = [
860
- ...buildWarnings,
861
- ...(visibility.warnings || []),
862
- ...(validation.warnings || [])
863
- ];
864
- recordManagedOmpSyncResult({
865
- path: paths.modelsYml,
866
- modelsPath: paths.modelsYml,
867
- settingsPath: visibility.settingsPath,
868
- statePath: visibility.statePath,
869
- backupPath,
870
- modelsBackupPath: backupPath,
871
- settingsBackupPath: visibility.settingsBackupPath,
872
- changed: true,
873
- visibilityChanged: visibility.changed,
874
- managedEnabledModels: visibility.managedEnabledModels,
875
- managedEnabledModelsForSettings: visibility.managedEnabledModelsForSettings,
876
- managedDisabledProviders: visibility.managedDisabledProviders,
877
- managedDefaultModel: visibility.managedDefaultModel,
878
- validation,
879
- warnings
1092
+ const orderedChannels = options.activeChannelId
1093
+ ? [
1094
+ ...channels.filter(channel => channel?.id === options.activeChannelId),
1095
+ ...channels.filter(channel => channel?.id !== options.activeChannelId)
1096
+ ]
1097
+ : channels;
1098
+ const prepared = options.gateway
1099
+ ? prepareManagedOmpChannels(orderedChannels, options.gateway)
1100
+ : { managedChannels: orderedChannels, unsupportedChannels: [] };
1101
+ const effectiveChannels = prepared.managedChannels;
1102
+ prepared.unsupportedChannels.forEach((channel) => {
1103
+ buildWarnings.push(`OMP channel "${channel.name || channel.id}" was not managed: ${channel.reason}.`);
880
1104
  });
881
- removeLegacyManagedExtension(paths);
882
- return paths.modelsYml;
883
- }
884
-
885
- function removeManagedOmpProviders(options = {}) {
886
- const paths = getOmpPaths();
887
- const config = readModelsConfig(paths.modelsYml);
888
- const visibility = removeManagedOmpVisibility(options);
889
- const before = Object.keys(config.providers || {}).length;
890
- pruneManagedProviders(config.providers);
891
- const after = Object.keys(config.providers || {}).length;
892
- let backupPath = null;
893
- let validation = {
894
- skipped: true,
895
- reason: before === after ? 'no-managed-providers' : 'not-run',
896
- warnings: []
897
- };
898
- if (before !== after) {
899
- backupPath = createModelsBackupIfNeeded(paths.modelsYml, options);
1105
+ try {
1106
+ const config = buildManagedModelsConfig(effectiveChannels, readModelsConfig(paths.modelsYml), {
1107
+ ...options,
1108
+ warnings: buildWarnings
1109
+ });
1110
+ const backupPath = createModelsBackupIfNeeded(paths.modelsYml, options);
900
1111
  writeModelsConfig(config, paths.modelsYml);
901
- validation = options.validateWithCli === true
1112
+ const visibility = syncManagedOmpVisibility(effectiveChannels, config, options);
1113
+ const validation = options.validateWithCli === true
902
1114
  ? validateOmpModelsConfig(options)
903
1115
  : { skipped: true, reason: 'cli-validation-disabled', warnings: [] };
904
- }
905
- recordManagedOmpSyncResult({
906
- path: paths.modelsYml,
907
- modelsPath: paths.modelsYml,
908
- settingsPath: visibility.settingsPath,
909
- statePath: visibility.statePath,
910
- backupPath,
911
- modelsBackupPath: backupPath,
912
- settingsBackupPath: visibility.settingsBackupPath,
913
- changed: before !== after,
914
- visibilityChanged: visibility.changed,
915
- stateRemoved: visibility.stateRemoved,
916
- validation,
917
- warnings: [
1116
+ const warnings = [
1117
+ ...buildWarnings,
918
1118
  ...(visibility.warnings || []),
919
1119
  ...(validation.warnings || [])
920
- ]
921
- });
922
- removeLegacyManagedExtension(paths);
1120
+ ];
1121
+ recordManagedOmpSyncResult({
1122
+ path: paths.modelsYml,
1123
+ modelsPath: paths.modelsYml,
1124
+ settingsPath: visibility.settingsPath,
1125
+ statePath: visibility.statePath,
1126
+ backupPath,
1127
+ modelsBackupPath: backupPath,
1128
+ settingsBackupPath: visibility.settingsBackupPath,
1129
+ changed: true,
1130
+ visibilityChanged: visibility.changed,
1131
+ managedEnabledModels: visibility.managedEnabledModels,
1132
+ managedEnabledModelsForSettings: visibility.managedEnabledModelsForSettings,
1133
+ managedDisabledProviders: visibility.managedDisabledProviders,
1134
+ managedDefaultModel: visibility.managedDefaultModel,
1135
+ validation,
1136
+ warnings
1137
+ });
1138
+ removeLegacyManagedExtension(paths);
1139
+ return paths.modelsYml;
1140
+ } catch (error) {
1141
+ restoreFileSnapshots(snapshots);
1142
+ throw error;
1143
+ }
1144
+ }
1145
+
1146
+ function removeManagedOmpProviders(options = {}) {
1147
+ const paths = getOmpPaths();
1148
+ const settingsPath = getOmpSettingsPath(paths);
1149
+ const statePath = getManagedVisibilityStatePath(paths);
1150
+ const snapshots = captureFileSnapshots([paths.modelsYml, settingsPath, statePath]);
1151
+ try {
1152
+ const config = readModelsConfig(paths.modelsYml);
1153
+ const visibility = removeManagedOmpVisibility(options);
1154
+ const before = Object.keys(config.providers || {}).length;
1155
+ pruneManagedProviders(config.providers);
1156
+ const after = Object.keys(config.providers || {}).length;
1157
+ let backupPath = null;
1158
+ let validation = {
1159
+ skipped: true,
1160
+ reason: before === after ? 'no-managed-providers' : 'not-run',
1161
+ warnings: []
1162
+ };
1163
+ if (before !== after) {
1164
+ backupPath = createModelsBackupIfNeeded(paths.modelsYml, options);
1165
+ writeModelsConfig(config, paths.modelsYml);
1166
+ validation = options.validateWithCli === true
1167
+ ? validateOmpModelsConfig(options)
1168
+ : { skipped: true, reason: 'cli-validation-disabled', warnings: [] };
1169
+ }
1170
+ recordManagedOmpSyncResult({
1171
+ path: paths.modelsYml,
1172
+ modelsPath: paths.modelsYml,
1173
+ settingsPath: visibility.settingsPath,
1174
+ statePath: visibility.statePath,
1175
+ backupPath,
1176
+ modelsBackupPath: backupPath,
1177
+ settingsBackupPath: visibility.settingsBackupPath,
1178
+ changed: before !== after,
1179
+ visibilityChanged: visibility.changed,
1180
+ stateRemoved: visibility.stateRemoved,
1181
+ validation,
1182
+ warnings: [
1183
+ ...(visibility.warnings || []),
1184
+ ...(validation.warnings || [])
1185
+ ]
1186
+ });
1187
+ removeLegacyManagedExtension(paths);
1188
+ } catch (error) {
1189
+ restoreFileSnapshots(snapshots);
1190
+ throw error;
1191
+ }
923
1192
  }
924
1193
 
925
1194
  function isManagedOmpProvidersActive() {