coding-tool-x 3.7.10 → 3.7.12

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 (54) hide show
  1. package/CHANGELOG.md +3 -0
  2. package/README.md +1 -1
  3. package/dist/web/assets/{Analytics-CkUtnoz9.js → Analytics-8dRv1aYW.js} +1 -1
  4. package/dist/web/assets/{ConfigTemplates-BwPqhE-N.js → ConfigTemplates-BQRXSICO.js} +1 -1
  5. package/dist/web/assets/Home-BtzWhKIa.js +1 -0
  6. package/dist/web/assets/Home-DYVy4cGZ.css +1 -0
  7. package/dist/web/assets/{PluginManager-JMXWl37b.js → PluginManager-BTa-Ygf6.js} +1 -1
  8. package/dist/web/assets/{ProjectList-CZwDgVvt.js → ProjectList-F_cB8ldA.js} +1 -1
  9. package/dist/web/assets/{SessionList-2fZmN7Au.js → SessionList-LOrbPQvu.js} +1 -1
  10. package/dist/web/assets/SkillManager-BsX8Qpj0.css +1 -0
  11. package/dist/web/assets/SkillManager-BzI_t-yV.js +1 -0
  12. package/dist/web/assets/{WorkspaceManager-B3lhhBD0.js → WorkspaceManager-Db9RGiz7.js} +1 -1
  13. package/dist/web/assets/index-DIWJmyL6.js +2 -0
  14. package/dist/web/assets/{index-C5nLTbOk.css → index-bJRG3RXH.css} +1 -1
  15. package/dist/web/index.html +2 -2
  16. package/docs/superpowers/plans/2026-08-01-omp-skill-scan-settings.md +759 -0
  17. package/docs/superpowers/specs/2026-08-01-omp-skill-scan-settings-design.md +224 -0
  18. package/package.json +1 -1
  19. package/src/commands/proxy-control.js +12 -12
  20. package/src/commands/toggle-proxy.js +16 -17
  21. package/src/commands/ui.js +11 -1
  22. package/src/config/default.js +1 -1
  23. package/src/config/paths.js +52 -29
  24. package/src/index.js +42 -16
  25. package/src/server/api/omp-channels.js +6 -4
  26. package/src/server/api/omp-config.js +1 -1
  27. package/src/server/api/omp-proxy.js +13 -42
  28. package/src/server/api/plugins.js +119 -47
  29. package/src/server/api/skills.js +63 -10
  30. package/src/server/index.js +3 -3
  31. package/src/server/omp-proxy-server.js +142 -10
  32. package/src/server/services/channel-scheduler.js +59 -9
  33. package/src/server/services/model-definition-schema.js +2 -1
  34. package/src/server/services/omp-auth-gateway-client.js +62 -0
  35. package/src/server/services/omp-channels.js +168 -12
  36. package/src/server/services/omp-config.js +71 -6
  37. package/src/server/services/omp-gateway-routing.js +295 -0
  38. package/src/server/services/omp-gateway.js +919 -0
  39. package/src/server/services/omp-native-plugin-adapter.js +456 -0
  40. package/src/server/services/omp-session-log-observer.js +138 -0
  41. package/src/server/services/omp-sessions.js +80 -0
  42. package/src/server/services/omp-settings-manager.js +338 -87
  43. package/src/server/services/omp-skill-discovery.js +358 -0
  44. package/src/server/services/omp-skill-settings-service.js +97 -0
  45. package/src/server/services/pi-omp-migration.js +273 -0
  46. package/src/server/services/platform-resolution.js +44 -0
  47. package/src/server/services/plugins-service.js +85 -561
  48. package/src/server/services/project-path-validation.js +65 -0
  49. package/src/server/services/skill-service.js +169 -77
  50. package/dist/web/assets/Home-BANl91zG.js +0 -1
  51. package/dist/web/assets/Home-CqnBXFxj.css +0 -1
  52. package/dist/web/assets/SkillManager-Bhzhkeis.js +0 -1
  53. package/dist/web/assets/SkillManager-D7pd-d_P.css +0 -1
  54. package/dist/web/assets/index-LYvv7wtx.js +0 -2
@@ -1,17 +1,97 @@
1
+ const crypto = require('crypto');
1
2
  const { loadConfig } = require('../config/loader');
2
3
  const { saveProxyStartTime, clearProxyStartTime, getProxyStartTime, getProxyRuntime } = require('./services/proxy-runtime');
3
- const { syncManagedOmpProviders, disableManagedOmpProviders, getEnabledChannels } = require('./services/omp-channels');
4
+ const {
5
+ syncManagedOmpProviders,
6
+ disableManagedOmpProviders,
7
+ activateStaticOmpChannel,
8
+ getEnabledChannels,
9
+ isManagedOmpModeEnabled,
10
+ enableManagedOmpMode,
11
+ disableManagedOmpMode,
12
+ loadManagedOmpActiveChannelId,
13
+ loadManagedOmpModeState
14
+ } = require('./services/omp-channels');
15
+ const {
16
+ startOmpSessionLogObserver,
17
+ stopOmpSessionLogObserver,
18
+ getOmpSessionLogObserverStatus
19
+ } = require('./services/omp-session-log-observer');
20
+ const { createOmpGateway } = require('./services/omp-gateway');
21
+ const { prepareManagedOmpChannels } = require('./services/omp-gateway-routing');
22
+ const { probeOmpAuthGateways } = require('./services/omp-auth-gateway-client');
4
23
 
5
- let running = false;
6
24
  let currentPort = null;
7
25
  let lastSyncResult = null;
26
+ let lastRoutingResult = { routes: [], unsupportedChannels: [] };
27
+ let lifecycle = Promise.resolve();
8
28
 
9
- async function startOmpProxyServer(options = {}) {
29
+ const gateway = createOmpGateway({
30
+ getChannels: () => getEnabledChannels()
31
+ });
32
+
33
+ function withLifecycleLock(operation) {
34
+ const next = lifecycle.then(operation, operation);
35
+ lifecycle = next.catch(() => {});
36
+ return next;
37
+ }
38
+
39
+ async function startOmpProxyServerUnlocked(options = {}) {
40
+ if (gateway.status().listening) {
41
+ return {
42
+ success: true,
43
+ port: gateway.status().port,
44
+ sync: lastSyncResult,
45
+ warnings: lastSyncResult?.warnings || []
46
+ };
47
+ }
10
48
  const preserveStartTime = options.preserveStartTime || false;
11
49
  const config = loadConfig();
12
- currentPort = config.ports?.ompProxy || 20092;
13
- lastSyncResult = syncManagedOmpProviders();
14
- running = true;
50
+ const wasEnabled = isManagedOmpModeEnabled();
51
+ const previousState = loadManagedOmpModeState?.() || {
52
+ activeChannelId: loadManagedOmpActiveChannelId()
53
+ };
54
+ const channels = getEnabledChannels();
55
+ const secret = crypto.randomBytes(32).toString('hex');
56
+ const desiredPort = options.port ?? config.ports?.ompProxy ?? 20092;
57
+ const gatewayStatus = await gateway.start({
58
+ host: '127.0.0.1',
59
+ port: desiredPort,
60
+ secret
61
+ });
62
+ currentPort = gatewayStatus.port;
63
+ try {
64
+ const oauthSupport = await probeOmpAuthGateways(channels);
65
+ const descriptor = {
66
+ ...gateway.descriptor(),
67
+ supportedOAuthChannelIds: oauthSupport.supportedOAuthChannelIds
68
+ };
69
+ lastRoutingResult = prepareManagedOmpChannels(channels, descriptor);
70
+ enableManagedOmpMode(options.activeChannelId, descriptor);
71
+ lastSyncResult = syncManagedOmpProviders(channels, {
72
+ gateway: descriptor,
73
+ activeChannelId: options.activeChannelId
74
+ });
75
+ if (oauthSupport.warnings.length > 0) {
76
+ lastSyncResult = {
77
+ ...lastSyncResult,
78
+ warnings: [
79
+ ...(lastSyncResult?.warnings || []),
80
+ ...oauthSupport.warnings
81
+ ]
82
+ };
83
+ }
84
+ } catch (error) {
85
+ if (wasEnabled) {
86
+ enableManagedOmpMode(previousState.activeChannelId, previousState.gateway || null);
87
+ } else {
88
+ disableManagedOmpMode();
89
+ }
90
+ await gateway.stop().catch(() => {});
91
+ currentPort = null;
92
+ throw error;
93
+ }
94
+ startOmpSessionLogObserver();
15
95
  saveProxyStartTime('omp', preserveStartTime);
16
96
  return {
17
97
  success: true,
@@ -21,12 +101,46 @@ async function startOmpProxyServer(options = {}) {
21
101
  };
22
102
  }
23
103
 
24
- async function stopOmpProxyServer(options = {}) {
104
+ function startOmpProxyServer(options = {}) {
105
+ return withLifecycleLock(() => startOmpProxyServerUnlocked(options));
106
+ }
107
+
108
+ async function stopOmpProxyServerUnlocked(options = {}) {
25
109
  const clearStartTime = options.clearStartTime !== false;
26
110
  const stoppedPort = currentPort;
27
- lastSyncResult = disableManagedOmpProviders();
28
- running = false;
111
+ if (!gateway.status().listening && !isManagedOmpModeEnabled()) {
112
+ return {
113
+ success: true,
114
+ port: stoppedPort,
115
+ sync: lastSyncResult,
116
+ warnings: lastSyncResult?.warnings || []
117
+ };
118
+ }
119
+ const state = loadManagedOmpModeState?.() || {
120
+ activeChannelId: loadManagedOmpActiveChannelId()
121
+ };
122
+ const enabledChannels = getEnabledChannels();
123
+ const activeChannel = enabledChannels.find(channel => channel.id === state.activeChannelId)
124
+ || enabledChannels[0]
125
+ || null;
126
+ gateway.beginDraining();
127
+ stopOmpSessionLogObserver();
128
+ try {
129
+ if (activeChannel) {
130
+ const handoff = activateStaticOmpChannel(activeChannel.id);
131
+ lastSyncResult = handoff?.sync || lastSyncResult;
132
+ } else {
133
+ lastSyncResult = disableManagedOmpProviders();
134
+ }
135
+ } catch (error) {
136
+ startOmpSessionLogObserver();
137
+ gateway.cancelDraining();
138
+ throw error;
139
+ }
140
+ disableManagedOmpMode();
141
+ await gateway.stop({ forceAfterMs: options.forceAfterMs });
29
142
  currentPort = null;
143
+ lastRoutingResult = { routes: [], unsupportedChannels: [] };
30
144
  if (clearStartTime) {
31
145
  clearProxyStartTime('omp');
32
146
  }
@@ -38,8 +152,14 @@ async function stopOmpProxyServer(options = {}) {
38
152
  };
39
153
  }
40
154
 
155
+ function stopOmpProxyServer(options = {}) {
156
+ return withLifecycleLock(() => stopOmpProxyServerUnlocked(options));
157
+ }
158
+
41
159
  function getOmpProxyStatus() {
42
160
  const config = loadConfig();
161
+ const gatewayStatus = gateway.status();
162
+ const running = gatewayStatus.listening;
43
163
  const startTime = getProxyStartTime('omp', { allowRecovery: running });
44
164
  const runtime = getProxyRuntime('omp', { allowRecovery: running });
45
165
  const enabledCount = getEnabledChannels().length;
@@ -50,11 +170,23 @@ function getOmpProxyStatus() {
50
170
  defaultPort: config.ports?.ompProxy || 20092,
51
171
  startTime,
52
172
  runtime,
53
- mode: 'models-yml-provider-config',
173
+ mode: 'http-gateway',
174
+ listening: gatewayStatus.listening,
175
+ draining: gatewayStatus.draining,
176
+ inflightRequests: gatewayStatus.inflightRequests,
177
+ routingGroups: lastRoutingResult.routes.map(route => ({
178
+ providerKey: route.providerKey,
179
+ providerApi: route.providerApi,
180
+ routingGroup: route.routingGroup,
181
+ managedProviderId: route.managedProviderId,
182
+ channelsCount: route.channelIds.length
183
+ })),
184
+ unsupportedChannels: lastRoutingResult.unsupportedChannels,
54
185
  enabledChannelsCount: enabledCount,
55
186
  modelsPath: lastSyncResult?.modelsPath || lastSyncResult?.path || null,
56
187
  backupPath: lastSyncResult?.backupPath || null,
57
188
  modelsValidation: validation,
189
+ sessionLogObserver: getOmpSessionLogObserverStatus(),
58
190
  warnings: lastSyncResult?.warnings || []
59
191
  };
60
192
  }
@@ -101,10 +101,35 @@ function refreshChannels(source = 'claude') {
101
101
  });
102
102
  }
103
103
 
104
- function getAvailableChannels(source = 'claude') {
104
+ function matchesAllocationRequest(channel, options = {}) {
105
+ const candidateIds = Array.isArray(options.candidateIds)
106
+ ? new Set(options.candidateIds.filter(Boolean))
107
+ : null;
108
+ const excludedIds = new Set(
109
+ Array.isArray(options.excludeChannelIds) ? options.excludeChannelIds.filter(Boolean) : []
110
+ );
111
+ if (candidateIds && !candidateIds.has(channel.id)) return false;
112
+ if (excludedIds.has(channel.id)) return false;
113
+ if (options.routingGroup && String(channel.routingGroup || channel.id) !== String(options.routingGroup)) {
114
+ return false;
115
+ }
116
+ if (options.providerKey && String(channel.providerKey || channel.provider || '') !== String(options.providerKey)) {
117
+ return false;
118
+ }
119
+ if (
120
+ options.providerApi
121
+ && String(channel.providerApi || channel.api || channel.wireApi || 'openai-completions') !== String(options.providerApi)
122
+ ) {
123
+ return false;
124
+ }
125
+ return true;
126
+ }
127
+
128
+ function getAvailableChannels(source = 'claude', options = {}) {
105
129
  refreshChannels(source);
106
130
  const state = getState(source);
107
131
  return state.channels.filter(ch => {
132
+ if (!matchesAllocationRequest(ch, options)) return false;
108
133
  if (!isChannelAvailable(ch.id, source)) {
109
134
  return false;
110
135
  }
@@ -132,7 +157,7 @@ function tryAllocate(source = 'claude', options = {}) {
132
157
  const state = getState(source);
133
158
  const sessionId = options.sessionId;
134
159
  const enableSessionBinding = options.enableSessionBinding !== false; // 默认开启
135
- const available = getAvailableChannels(source);
160
+ const available = getAvailableChannels(source, options);
136
161
  if (!available.length) {
137
162
  return null;
138
163
  }
@@ -169,6 +194,7 @@ function drainQueue(source = 'claude') {
169
194
  if (channel) {
170
195
  clearTimeout(entry.timer);
171
196
  state.queue.splice(i, 1);
197
+ entry.cleanup?.();
172
198
  entry.resolve(channel);
173
199
  return drainQueue(source);
174
200
  }
@@ -178,6 +204,9 @@ function drainQueue(source = 'claude') {
178
204
  function allocateChannel(options = {}) {
179
205
  const source = options.source || 'claude';
180
206
  const state = getState(source);
207
+ if (options.signal?.aborted) {
208
+ return Promise.reject(Object.assign(new Error('Channel allocation aborted'), { name: 'AbortError' }));
209
+ }
181
210
  const channel = tryAllocate(source, options);
182
211
  if (channel) {
183
212
  return Promise.resolve(channel);
@@ -186,16 +215,31 @@ function allocateChannel(options = {}) {
186
215
  if (!state.channels.length) {
187
216
  return Promise.reject(new Error('暂无可用渠道,请先添加并启用至少一个渠道'));
188
217
  }
218
+ if (!state.channels.some(ch => matchesAllocationRequest(ch, options))) {
219
+ return Promise.reject(new Error('没有匹配当前请求的可用渠道'));
220
+ }
189
221
 
190
222
  // 检查是否所有渠道都被冻结
191
223
  const allFrozen = state.channels.every(ch => !isChannelAvailable(ch.id, source));
192
224
 
193
225
  return new Promise((resolve, reject) => {
226
+ let entry;
227
+ const cleanup = () => {
228
+ options.signal?.removeEventListener('abort', onAbort);
229
+ };
230
+ const removeEntry = () => {
231
+ const index = state.queue.indexOf(entry);
232
+ if (index !== -1) state.queue.splice(index, 1);
233
+ };
234
+ const onAbort = () => {
235
+ clearTimeout(timer);
236
+ removeEntry();
237
+ cleanup();
238
+ reject(Object.assign(new Error('Channel allocation aborted'), { name: 'AbortError' }));
239
+ };
194
240
  const timer = setTimeout(() => {
195
- const index = state.queue.findIndex(item => item.timer === timer);
196
- if (index !== -1) {
197
- state.queue.splice(index, 1);
198
- }
241
+ removeEntry();
242
+ cleanup();
199
243
  // 根据实际情况返回更准确的错误信息
200
244
  if (allFrozen) {
201
245
  reject(new Error('所有渠道均已被冻结,请等待健康检查恢复或手动重置'));
@@ -204,13 +248,19 @@ function allocateChannel(options = {}) {
204
248
  }
205
249
  }, WAIT_TIMEOUT_MS);
206
250
 
207
- state.queue.push({
251
+ entry = {
208
252
  source,
209
253
  options,
210
254
  resolve,
211
255
  reject,
212
- timer
213
- });
256
+ timer,
257
+ cleanup
258
+ };
259
+ state.queue.push(entry);
260
+ options.signal?.addEventListener('abort', onAbort, { once: true });
261
+ if (options.signal?.aborted) {
262
+ onAbort();
263
+ }
214
264
  });
215
265
  }
216
266
 
@@ -390,7 +390,8 @@ function normalizeCatalogModelList(value) {
390
390
  const warnings = [];
391
391
  for (const rawModel of Array.isArray(value) ? value : []) {
392
392
  const rawId = String(rawModel?.id || rawModel?.name || '').trim();
393
- const normalized = normalizeModelSpec(rawModel, rawId);
393
+ const { provider: _provider, selector: _selector, ...modelFields } = isPlainObject(rawModel) ? rawModel : {};
394
+ const normalized = normalizeModelSpec(modelFields, rawId);
394
395
  if (!normalized.spec?.id) continue;
395
396
  warnings.push(...normalized.warnings.map(message => `${normalized.spec.id}: ${message}`));
396
397
  const key = normalized.spec.id.toLowerCase();
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ const http = require('http');
4
+ const https = require('https');
5
+
6
+ function isPiNativeOAuthChannel(channel = {}) {
7
+ const transport = String(channel.transport || channel.providerConfig?.transport || '').trim();
8
+ return channel.authMode === 'oauth'
9
+ && transport === 'pi-native'
10
+ && Boolean(String(channel.apiKey || '').trim())
11
+ && Boolean(String(channel.baseUrl || '').trim());
12
+ }
13
+
14
+ function probeHealth(channel, timeoutMs) {
15
+ return new Promise((resolve) => {
16
+ let target;
17
+ try {
18
+ target = new URL('/healthz', channel.baseUrl);
19
+ } catch {
20
+ resolve(false);
21
+ return;
22
+ }
23
+ const transport = target.protocol === 'https:' ? https : http;
24
+ const request = transport.request(target, {
25
+ method: 'GET',
26
+ headers: {
27
+ accept: 'application/json'
28
+ }
29
+ }, (response) => {
30
+ response.resume();
31
+ response.once('end', () => {
32
+ resolve((response.statusCode || 0) >= 200 && (response.statusCode || 0) < 300);
33
+ });
34
+ });
35
+ request.setTimeout(timeoutMs, () => {
36
+ request.destroy(new Error('OMP auth gateway health probe timed out'));
37
+ });
38
+ request.once('error', () => resolve(false));
39
+ request.end();
40
+ });
41
+ }
42
+
43
+ async function probeOmpAuthGateways(channels = [], options = {}) {
44
+ const candidates = channels.filter(isPiNativeOAuthChannel);
45
+ const timeoutMs = Math.max(100, Number(options.timeoutMs) || 1000);
46
+ const results = await Promise.all(candidates.map(async (channel) => ({
47
+ channel,
48
+ healthy: await probeHealth(channel, timeoutMs)
49
+ })));
50
+ return {
51
+ supportedOAuthChannelIds: results
52
+ .filter(result => result.healthy)
53
+ .map(result => result.channel.id),
54
+ warnings: results
55
+ .filter(result => !result.healthy)
56
+ .map(result => `OMP OAuth auth gateway for channel "${result.channel.name || result.channel.id}" is unavailable; the native OMP provider remains active.`)
57
+ };
58
+ }
59
+
60
+ module.exports = {
61
+ probeOmpAuthGateways
62
+ };
@@ -1,13 +1,12 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { execFileSync } = require('child_process');
4
- const { PATHS } = require('../../config/paths');
4
+ const { PATHS, ensureStorageDirMigrated } = require('../../config/paths');
5
5
  const BaseChannelService = require('./base/base-channel-service');
6
6
  const ompConfig = require('./omp-config');
7
7
  const {
8
8
  writeManagedOmpProviders,
9
9
  removeManagedOmpProviders,
10
- isManagedOmpProvidersActive,
11
10
  getLastManagedOmpSyncResult,
12
11
  readModelsConfig,
13
12
  readOmpSettingsConfig,
@@ -23,13 +22,100 @@ const {
23
22
 
24
23
  const OMP_THINKING_SUFFIX_RE = /:(minimal|low|medium|high|xhigh|off)$/;
25
24
 
25
+ function clearOmpChannelBalanceCache(channel) {
26
+ try {
27
+ require('./channel-balance').clearChannelBalanceCache('omp', channel);
28
+ } catch (_) {
29
+ // Balance cache invalidation is an optimization; channel activation must still succeed.
30
+ }
31
+ }
32
+
33
+ function selectLatestEnabledChannel(channels = []) {
34
+ const enabledChannels = (channels || []).filter(channel => channel?.enabled !== false);
35
+ if (enabledChannels.length === 0) return null;
36
+ return enabledChannels.reduce((latest, current) => {
37
+ const latestTs = Number(latest?.updatedAt || latest?.createdAt || 0);
38
+ const currentTs = Number(current?.updatedAt || current?.createdAt || 0);
39
+ return currentTs > latestTs ? current : latest;
40
+ }, enabledChannels[0]);
41
+ }
42
+
43
+ function loadManagedOmpModeState() {
44
+ ensureStorageDirMigrated();
45
+ try {
46
+ if (!fs.existsSync(PATHS.activeChannel.omp)) return null;
47
+ const data = JSON.parse(fs.readFileSync(PATHS.activeChannel.omp, 'utf8'));
48
+ return data && typeof data === 'object' && !Array.isArray(data) ? data : null;
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ function loadManagedOmpActiveChannelId() {
55
+ return loadManagedOmpModeState()?.activeChannelId || null;
56
+ }
57
+
58
+ function isManagedOmpModeEnabled() {
59
+ ensureStorageDirMigrated();
60
+ return fs.existsSync(PATHS.activeChannel.omp);
61
+ }
62
+
63
+ function writeManagedOmpModeState(filePath, state) {
64
+ const temporaryPath = `${filePath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
65
+ try {
66
+ fs.writeFileSync(temporaryPath, JSON.stringify(state, null, 2), {
67
+ encoding: 'utf8',
68
+ mode: 0o600
69
+ });
70
+ fs.renameSync(temporaryPath, filePath);
71
+ } catch (error) {
72
+ if (fs.existsSync(temporaryPath)) fs.unlinkSync(temporaryPath);
73
+ throw error;
74
+ }
75
+ }
76
+
77
+ function enableManagedOmpMode(activeChannelId = null, gateway = null) {
78
+ ensureStorageDirMigrated();
79
+ const filePath = PATHS.activeChannel.omp;
80
+ const dir = path.dirname(filePath);
81
+ if (!fs.existsSync(dir)) {
82
+ fs.mkdirSync(dir, { recursive: true });
83
+ }
84
+ const nextActiveChannelId = activeChannelId || loadManagedOmpActiveChannelId();
85
+ const previousState = loadManagedOmpModeState();
86
+ const nextGateway = gateway || previousState?.gateway || null;
87
+ const state = {
88
+ version: 2,
89
+ activeChannelId: nextActiveChannelId || null
90
+ };
91
+ if (nextGateway) {
92
+ state.gateway = {
93
+ host: String(nextGateway.host || '127.0.0.1'),
94
+ port: Number(nextGateway.port),
95
+ secret: String(nextGateway.secret || ''),
96
+ supportedOAuthChannelIds: Array.isArray(nextGateway.supportedOAuthChannelIds)
97
+ ? [...nextGateway.supportedOAuthChannelIds]
98
+ : []
99
+ };
100
+ }
101
+ writeManagedOmpModeState(filePath, state);
102
+ return nextActiveChannelId || null;
103
+ }
104
+
105
+ function disableManagedOmpMode() {
106
+ ensureStorageDirMigrated();
107
+ if (fs.existsSync(PATHS.activeChannel.omp)) {
108
+ fs.unlinkSync(PATHS.activeChannel.omp);
109
+ }
110
+ }
111
+
26
112
  class OmpChannelService extends BaseChannelService {
27
113
  constructor() {
28
114
  super({
29
115
  platform: 'omp',
30
116
  channelsFilePath: PATHS.channels.omp,
31
117
  defaultGatewaySource: 'openai_compatible',
32
- isProxyRunning: () => isManagedOmpProvidersActive()
118
+ isProxyRunning: () => isManagedOmpModeEnabled()
33
119
  });
34
120
  }
35
121
 
@@ -37,10 +123,16 @@ class OmpChannelService extends BaseChannelService {
37
123
  const normalized = super._applyDefaults(channel);
38
124
  normalized.providerKey = normalized.providerKey || normalized.provider || normalized.name || normalized.id;
39
125
  normalized.providerApi = normalized.providerApi || normalized.api || normalized.wireApi || 'openai-completions';
40
- normalized.authMode = normalized.authMode === 'oauth' ? 'oauth' : 'api_key';
126
+ normalized.authMode = ['api_key', 'oauth', 'none'].includes(normalized.authMode)
127
+ ? normalized.authMode
128
+ : 'api_key';
129
+ normalized.routingGroup = String(normalized.routingGroup || '').trim();
41
130
  normalized.oauthProviderId = normalized.oauthProviderId || (normalized.authMode === 'oauth' ? normalized.providerKey : '');
42
131
  if (normalized.authMode === 'oauth') {
43
132
  normalized.apiKey = normalized.apiKey || '';
133
+ } else if (normalized.authMode === 'none') {
134
+ normalized.apiKey = '';
135
+ normalized.oauthProviderId = '';
44
136
  }
45
137
  normalized.model = normalized.model || null;
46
138
  normalized.models = Array.isArray(normalized.models) ? normalized.models : [];
@@ -59,28 +151,49 @@ class OmpChannelService extends BaseChannelService {
59
151
  }
60
152
 
61
153
  _applyToNativeSettings(channel) {
62
- writeManagedOmpProviders([channel]);
154
+ if (isManagedOmpModeEnabled()) {
155
+ const state = loadManagedOmpModeState();
156
+ enableManagedOmpMode(channel.id, state?.gateway || null);
157
+ writeManagedOmpProviders([channel], state?.gateway ? {
158
+ gateway: state.gateway,
159
+ activeChannelId: channel.id
160
+ } : {});
161
+ return;
162
+ }
163
+ writeManagedOmpProviders([channel], {});
63
164
  }
64
165
 
65
166
  _onAfterCreate(_channel, allChannels) {
66
- this.syncManagedOmpProviders(allChannels);
167
+ this.syncOmpProvidersForCurrentMode(allChannels);
67
168
  }
68
169
 
69
170
  _onAfterUpdate(_oldChannel, _newChannel, allChannels) {
70
- this.syncManagedOmpProviders(allChannels);
171
+ this.syncOmpProvidersForCurrentMode(allChannels);
71
172
  }
72
173
 
73
174
  _onAfterDelete(_channel, allChannels) {
74
- this.syncManagedOmpProviders(allChannels);
175
+ this.syncOmpProvidersForCurrentMode(allChannels);
176
+ }
177
+
178
+ syncOmpProvidersForCurrentMode(channels = this.getChannels().channels) {
179
+ if (isManagedOmpModeEnabled()) {
180
+ const state = loadManagedOmpModeState();
181
+ return this.syncManagedOmpProviders(channels, state?.gateway ? {
182
+ gateway: state.gateway,
183
+ activeChannelId: state.activeChannelId
184
+ } : {});
185
+ }
186
+ const activeChannel = selectLatestEnabledChannel(channels);
187
+ return this.syncManagedOmpProviders(activeChannel ? [activeChannel] : [], {});
75
188
  }
76
189
 
77
- syncManagedOmpProviders(channels = this.getChannels().channels) {
190
+ syncManagedOmpProviders(channels = this.getChannels().channels, options = {}) {
78
191
  const enabledChannels = (channels || []).filter(channel => channel.enabled !== false);
79
192
  if (enabledChannels.length === 0) {
80
193
  removeManagedOmpProviders();
81
194
  return getLastManagedOmpSyncResult();
82
195
  }
83
- writeManagedOmpProviders(enabledChannels);
196
+ writeManagedOmpProviders(enabledChannels, options);
84
197
  return getLastManagedOmpSyncResult();
85
198
  }
86
199
 
@@ -89,8 +202,45 @@ class OmpChannelService extends BaseChannelService {
89
202
  return getLastManagedOmpSyncResult();
90
203
  }
91
204
 
205
+ activateStaticOmpChannel(channelId) {
206
+ const data = this.loadChannels();
207
+ const channel = data.channels.find(item => item.id === channelId);
208
+ if (!channel) {
209
+ throw new Error('Channel not found');
210
+ }
211
+
212
+ const previousData = JSON.parse(JSON.stringify(data));
213
+ const wasEnabled = channel.enabled !== false;
214
+ data.channels.forEach(item => {
215
+ item.enabled = item.id === channelId;
216
+ });
217
+
218
+ try {
219
+ this.saveChannels(data);
220
+ const sync = this.syncManagedOmpProviders([channel], {});
221
+ if (!wasEnabled) {
222
+ clearOmpChannelBalanceCache(channel);
223
+ }
224
+ return { channel, sync };
225
+ } catch (error) {
226
+ try {
227
+ this.saveChannels(previousData);
228
+ } catch (rollbackError) {
229
+ error.rollbackError = rollbackError;
230
+ }
231
+ throw error;
232
+ }
233
+ }
234
+
235
+ applyChannelToSettings(channelId) {
236
+ if (isManagedOmpModeEnabled()) {
237
+ return super.applyChannelToSettings(channelId);
238
+ }
239
+ return this.activateStaticOmpChannel(channelId).channel;
240
+ }
241
+
92
242
  syncManagedProviderExtension(channels = this.getChannels().channels) {
93
- return this.syncManagedOmpProviders(channels);
243
+ return this.syncOmpProvidersForCurrentMode(channels);
94
244
  }
95
245
 
96
246
  disableManagedProviderExtension() {
@@ -394,11 +544,17 @@ module.exports = {
394
544
  getEnabledChannels: () => service.getEnabledChannels(),
395
545
  saveChannelOrder: (order) => service.saveChannelOrder(order),
396
546
  applyChannelToSettings: (id) => service.applyChannelToSettings(id),
547
+ activateStaticOmpChannel: (id) => service.activateStaticOmpChannel(id),
397
548
  disableAllChannels: () => service.disableAllChannels(),
398
549
  getEffectiveApiKey: (channel) => channel?.apiKey || null,
399
- syncManagedOmpProviders: (channels) => service.syncManagedOmpProviders(channels),
550
+ syncManagedOmpProviders: (channels, options) => service.syncManagedOmpProviders(channels, options),
400
551
  disableManagedOmpProviders: () => service.disableManagedOmpProviders(),
401
552
  syncManagedProviderExtension: (channels) => service.syncManagedProviderExtension(channels),
402
553
  disableManagedProviderExtension: () => service.disableManagedProviderExtension(),
554
+ isManagedOmpModeEnabled,
555
+ enableManagedOmpMode,
556
+ disableManagedOmpMode,
557
+ loadManagedOmpModeState,
558
+ loadManagedOmpActiveChannelId,
403
559
  syncCurrentOmpChannel
404
560
  };