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
@@ -5,14 +5,14 @@
5
5
  <link rel="icon" href="/favicon.ico">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
7
  <title>coding-tool-x · AI 编程工作台</title>
8
- <script type="module" crossorigin src="/assets/index-CCP7sRMP.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-k5oP7c-A.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/markdown-BNJfEgCA.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/vue-vendor-B5RqjEjQ.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/naive-ui-BALVpMgd.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/icons-nGdSKQn2.js">
13
13
  <link rel="modulepreload" crossorigin href="/assets/vendors-DQA4Hw9v.js">
14
14
  <link rel="stylesheet" crossorigin href="/assets/markdown-BfC0goYb.css">
15
- <link rel="stylesheet" crossorigin href="/assets/index-C5nLTbOk.css">
15
+ <link rel="stylesheet" crossorigin href="/assets/index-BceXgSSe.css">
16
16
  </head>
17
17
  <body>
18
18
  <div id="app"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-tool-x",
3
- "version": "3.7.9",
3
+ "version": "3.7.11",
4
4
  "description": "Vibe Coding 增强工作助手 - 智能会话管理、动态渠道切换、全局搜索、实时监控",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -205,9 +205,10 @@ async function handleStartProxy(cliType, services) {
205
205
 
206
206
  console.log(chalk.cyan('动态切换功能说明:'));
207
207
  if (services.managedProviderConfig) {
208
- console.log(chalk.gray('• 开启后会把已启用的 OMP 渠道写入受管 models.yml provider 配置'));
209
- console.log(chalk.gray(`• 可以通过 Web UI 或"渠道管理"功能调整启用的 ${toolName} provider`));
210
- console.log(chalk.gray(`• 状态端口: http://127.0.0.1:${defaultPort}\n`));
208
+ console.log(chalk.gray('• 开启后会启动 OMP 专用本地网关,并把受管 provider 指向该网关'));
209
+ console.log(chalk.gray(`• 可以通过 Web UI 或"渠道管理"功能调整启用的 ${toolName} 路由组`));
210
+ console.log(chalk.gray('• 同路由组内由 coding-tool-x 动态选择渠道,跨模型 fallback 仍由 OMP 处理'));
211
+ console.log(chalk.gray(`• OMP 网关地址: http://127.0.0.1:${defaultPort}\n`));
211
212
  } else {
212
213
  console.log(chalk.gray('• 开启后会在本地启动一个代理服务'));
213
214
  console.log(chalk.gray(`• 可以在不重启 ${toolName} 的情况下动态管理渠道`));
@@ -217,7 +218,7 @@ async function handleStartProxy(cliType, services) {
217
218
 
218
219
  console.log(chalk.yellow('[WARN] 重要提示:'));
219
220
  if (services.managedProviderConfig) {
220
- console.log(chalk.yellow('• 启用后会写入 Coding-Tool 管理的 OMP models.yml provider 配置'));
221
+ console.log(chalk.yellow('• 启用后会写入指向本地网关的 OMP models.yml provider 配置'));
221
222
  console.log(chalk.yellow('• 关闭会移除受管 provider,不会清理 OMP 原生 auth.json\n'));
222
223
  } else {
223
224
  console.log(chalk.yellow('• 开启期间请勿关闭 CLI 终端窗口'));
@@ -253,9 +254,7 @@ async function handleStartProxy(cliType, services) {
253
254
 
254
255
  if (services.managedProviderConfig) {
255
256
  console.log(chalk.green('[OK] OMP models.yml 受管 provider 已启用'));
256
- if (proxyResult.port) {
257
- console.log(chalk.gray(`状态端口: http://127.0.0.1:${proxyResult.port}`));
258
- }
257
+ console.log(chalk.gray('OMP 新增会话用量日志观察已启用'));
259
258
  (proxyResult.warnings || []).forEach((warning) => {
260
259
  console.log(chalk.yellow(`[WARN] ${warning}`));
261
260
  });
@@ -325,7 +324,7 @@ async function handleStopProxy(cliType, services) {
325
324
  if (services.managedProviderConfig) {
326
325
  console.log(chalk.gray(`• 受管配置: ${chalk.green('已启用')}`));
327
326
  console.log(chalk.gray(`• 配置模式: ${proxyStatus.mode || 'models-yml-provider-config'}`));
328
- console.log(chalk.gray(`• 状态端口: http://127.0.0.1:${proxyStatus.port || proxyStatus.defaultPort}\n`));
327
+ console.log(chalk.gray(`• 会话日志观察: ${proxyStatus.sessionLogObserver?.running ? chalk.green('运行中') : chalk.yellow('未运行')}\n`));
329
328
  } else {
330
329
  console.log(chalk.gray(`• 代理服务: ${chalk.green('运行中')}`));
331
330
  console.log(chalk.gray(`• 代理端口: ${proxyStatus.port}`));
@@ -87,6 +87,16 @@ async function handleUI() {
87
87
  // 忽略错误
88
88
  }
89
89
 
90
+ // OMP 动态网关持有 models.yml 事务,退出前必须先恢复原生配置。
91
+ try {
92
+ const { getOmpProxyStatus, stopOmpProxyServer } = require('../server/omp-proxy-server');
93
+ if (getOmpProxyStatus().running) {
94
+ await stopOmpProxyServer({ forceAfterMs: 1500 });
95
+ }
96
+ } catch (err) {
97
+ console.error(chalk.yellow(`[WARN] OMP 动态网关退出清理失败: ${err.message}`));
98
+ }
99
+
90
100
  console.log(chalk.green('[OK] Web UI 已停止\n'));
91
101
  process.exit(0);
92
102
  });
@@ -99,7 +109,7 @@ async function handleUI() {
99
109
 
100
110
  } catch (error) {
101
111
  console.error(chalk.red('启动服务器失败:'), error.message);
102
- process.exit(1);
112
+ throw error;
103
113
  }
104
114
  }
105
115
 
@@ -21,7 +21,7 @@ const DEFAULT_CONFIG = {
21
21
  codexProxy: 20089, // Codex 代理服务端口
22
22
  geminiProxy: 20090, // Gemini 代理服务端口
23
23
  opencodeProxy: 20091, // OpenCode 代理服务端口
24
- ompProxy: 20092 // OMP 托管 provider 状态端口占位
24
+ ompProxy: 20092 // OMP 专用动态网关端口
25
25
  },
26
26
  maxLogs: 100,
27
27
  statsInterval: 30,
package/src/index.js CHANGED
@@ -34,9 +34,7 @@ function getVersion() {
34
34
  }
35
35
 
36
36
  function finishCli(code = 0) {
37
- eventBus.emitSync('cli:shutdown', {});
38
- PluginManager.shutdownPlugins();
39
- process.exit(code);
37
+ return shutdownProcess(code);
40
38
  }
41
39
 
42
40
  // 显示帮助信息
@@ -128,20 +126,49 @@ function showHelp() {
128
126
  console.log(chalk.gray(' 问题: https://github.com/ZeaoZhang/coding-tool/issues\n'));
129
127
  }
130
128
 
129
+ let processShutdownPromise = null;
130
+
131
+ async function stopOwnedOmpGatewayBeforeExit() {
132
+ try {
133
+ const {
134
+ getOmpProxyStatus,
135
+ stopOmpProxyServer
136
+ } = require('./server/omp-proxy-server');
137
+ if (getOmpProxyStatus().running) {
138
+ await stopOmpProxyServer({ forceAfterMs: 1500 });
139
+ }
140
+ } catch (error) {
141
+ console.error(chalk.yellow(`[WARN] OMP 动态网关退出清理失败: ${error.message}`));
142
+ }
143
+ }
144
+
145
+ function shutdownProcess(code = 0, error = null) {
146
+ if (processShutdownPromise) return processShutdownPromise;
147
+ processShutdownPromise = (async () => {
148
+ await stopOwnedOmpGatewayBeforeExit();
149
+ eventBus.emitSync('cli:shutdown', {});
150
+ PluginManager.shutdownPlugins();
151
+ if (error) {
152
+ console.error(error);
153
+ }
154
+ process.exit(code);
155
+ })();
156
+ return processShutdownPromise;
157
+ }
158
+
131
159
  // 全局错误处理
132
160
  process.on('uncaughtException', (err) => {
133
161
  // 忽略终端相关的错误(通常在 Ctrl+C 时发生)
134
- if (err.code === 'EIO' || err.code === 'ENOTTY' || err.code === 'EPIPE') {
135
- process.exit(0);
136
- }
137
- throw err;
162
+ const terminalError = err.code === 'EIO' || err.code === 'ENOTTY' || err.code === 'EPIPE';
163
+ void shutdownProcess(terminalError ? 0 : 1, terminalError ? null : err);
138
164
  });
139
165
 
140
- // 处理 SIGINT 信号(Ctrl+C)
141
- process.on('SIGINT', async () => {
142
- eventBus.emitSync('cli:shutdown', {});
143
- PluginManager.shutdownPlugins();
144
- process.exit(0);
166
+ // 处理进程退出信号,OMP 必须先排空并恢复原生配置。
167
+ process.on('SIGINT', () => {
168
+ void shutdownProcess(0);
169
+ });
170
+ process.on('SIGTERM', () => {
171
+ void shutdownProcess(0);
145
172
  });
146
173
 
147
174
  /**
@@ -416,7 +443,7 @@ async function main() {
416
443
  eventBus.emitSync('cli:command:after', { command: args[0], args: args.slice(1), result });
417
444
  if (!result.success) {
418
445
  console.error(chalk.red(result.error));
419
- process.exit(1);
446
+ return shutdownProcess(1);
420
447
  }
421
448
  return;
422
449
  }
@@ -712,6 +739,5 @@ async function main() {
712
739
 
713
740
  // 启动应用
714
741
  main().catch((error) => {
715
- console.error('程序出错:', error);
716
- process.exit(1);
742
+ void shutdownProcess(1, error);
717
743
  });
@@ -294,7 +294,7 @@ module.exports = () => {
294
294
  baseUrl,
295
295
  apiKey: apiKey || '',
296
296
  gatewaySourceType: gatewaySourceType || 'openai_compatible',
297
- authMode: authMode === 'oauth' ? 'oauth' : 'api_key',
297
+ authMode: ['api_key', 'oauth', 'none'].includes(authMode) ? authMode : 'api_key',
298
298
  oauthProviderId: oauthProviderId || '',
299
299
  model: model || null,
300
300
  speedTestModel: speedTestModel || null,
@@ -356,9 +356,18 @@ module.exports = () => {
356
356
  if (!providerKey) {
357
357
  return res.status(400).json({ error: 'providerKey is required' });
358
358
  }
359
+ const requestedModelIds = uniqueModels([
360
+ req.body?.model,
361
+ req.body?.speedTestModel,
362
+ ...(Array.isArray(req.body?.allowedModels) ? req.body.allowedModels : []),
363
+ ...(Array.isArray(req.body?.models)
364
+ ? req.body.models.map(model => typeof model === 'string' ? model : (model?.id || model?.name))
365
+ : [])
366
+ ]);
359
367
  const rawModels = getOmpCatalogModels(providerKey, {
360
368
  forceCatalogRefresh: req.body?.forceRefresh === true,
361
- catalogTimeout: 5000
369
+ catalogTimeout: 5000,
370
+ requestedModelIds
362
371
  });
363
372
  const normalizedCatalog = normalizeCatalogModelList(rawModels);
364
373
  const models = normalizedCatalog.models.map(model => redactSensitiveFields(model));
@@ -398,6 +407,7 @@ module.exports = () => {
398
407
  providerApi,
399
408
  authMode,
400
409
  oauthProviderId,
410
+ routingGroup,
401
411
  presetId,
402
412
  websiteUrl,
403
413
  balanceToken,
@@ -416,8 +426,8 @@ module.exports = () => {
416
426
  if (!name || !baseUrl) {
417
427
  return res.status(400).json({ error: 'Missing required fields: name and baseUrl' });
418
428
  }
419
- const normalizedAuthMode = authMode === 'oauth' ? 'oauth' : 'api_key';
420
- if (!apiKey && normalizedAuthMode !== 'oauth') {
429
+ const normalizedAuthMode = ['api_key', 'oauth', 'none'].includes(authMode) ? authMode : 'api_key';
430
+ if (!apiKey && normalizedAuthMode === 'api_key') {
421
431
  return res.status(400).json({ error: 'API Key is required' });
422
432
  }
423
433
 
@@ -426,7 +436,8 @@ module.exports = () => {
426
436
  providerApi: providerApi || wireApi || 'openai-completions',
427
437
  providerKey,
428
438
  authMode: normalizedAuthMode,
429
- oauthProviderId: oauthProviderId || providerKey || '',
439
+ oauthProviderId: normalizedAuthMode === 'oauth' ? (oauthProviderId || providerKey || '') : '',
440
+ routingGroup: String(routingGroup || '').trim(),
430
441
  enabled,
431
442
  weight,
432
443
  maxConcurrency,
@@ -81,7 +81,7 @@ function buildCapabilities() {
81
81
  },
82
82
  notes: [
83
83
  'The omp channel is backed by OMP and uses omp as its platform key.',
84
- 'Provider channels are enabled by merging managed ctx-* providers into OMP models.yml.'
84
+ 'Dynamic mode exposes managed ctx-* providers through the loopback OMP HTTP gateway; disabling it restores native OMP configuration.'
85
85
  ]
86
86
  };
87
87
  }
@@ -1,6 +1,4 @@
1
1
  const express = require('express');
2
- const fs = require('fs');
3
- const path = require('path');
4
2
  const router = express.Router();
5
3
  const {
6
4
  startOmpProxyServer,
@@ -10,10 +8,10 @@ const {
10
8
  const {
11
9
  getChannels,
12
10
  getEnabledChannels,
13
- markChannelAsRecentlyUsed
11
+ markChannelAsRecentlyUsed,
12
+ loadManagedOmpActiveChannelId
14
13
  } = require('../services/omp-channels');
15
14
  const { getSchedulerState } = require('../services/channel-scheduler');
16
- const { PATHS, ensureStorageDirMigrated } = require('../../config/paths');
17
15
 
18
16
  function sanitizeChannel(channel) {
19
17
  if (!channel) return null;
@@ -21,7 +19,10 @@ function sanitizeChannel(channel) {
21
19
  id: channel.id,
22
20
  name: channel.name,
23
21
  baseUrl: channel.baseUrl,
24
- websiteUrl: channel.websiteUrl
22
+ websiteUrl: channel.websiteUrl,
23
+ providerKey: channel.providerKey,
24
+ providerApi: channel.providerApi,
25
+ routingGroup: channel.routingGroup || ''
25
26
  };
26
27
  }
27
28
 
@@ -35,34 +36,6 @@ function selectLatestEnabledChannel(channels) {
35
36
  }, enabledChannels[0]);
36
37
  }
37
38
 
38
- function saveActiveChannelId(channelId) {
39
- ensureStorageDirMigrated();
40
- const filePath = PATHS.activeChannel.omp;
41
- const dir = path.dirname(filePath);
42
- if (!fs.existsSync(dir)) {
43
- fs.mkdirSync(dir, { recursive: true });
44
- }
45
- fs.writeFileSync(filePath, JSON.stringify({ activeChannelId: channelId }, null, 2), 'utf8');
46
- }
47
-
48
- function loadActiveChannelId() {
49
- ensureStorageDirMigrated();
50
- try {
51
- if (!fs.existsSync(PATHS.activeChannel.omp)) return null;
52
- const data = JSON.parse(fs.readFileSync(PATHS.activeChannel.omp, 'utf8'));
53
- return data.activeChannelId || null;
54
- } catch {
55
- return null;
56
- }
57
- }
58
-
59
- function removeActiveChannelFile() {
60
- ensureStorageDirMigrated();
61
- if (fs.existsSync(PATHS.activeChannel.omp)) {
62
- fs.unlinkSync(PATHS.activeChannel.omp);
63
- }
64
- }
65
-
66
39
  function resolveActiveChannel(channels, activeChannelId = null) {
67
40
  if (!Array.isArray(channels) || channels.length === 0) return null;
68
41
  if (activeChannelId) {
@@ -80,12 +53,12 @@ router.get('/status', (req, res) => {
80
53
  const proxy = getOmpProxyStatus();
81
54
  const { channels } = getChannels();
82
55
  const enabledChannels = channels.filter(ch => ch.enabled !== false);
83
- const activeChannel = resolveActiveChannel(channels, loadActiveChannelId());
56
+ const activeChannel = resolveActiveChannel(channels, loadManagedOmpActiveChannelId());
84
57
  res.json({
85
58
  proxy,
86
59
  config: {
87
- mode: 'models-yml-provider-config',
88
- nativeProxyProtocol: false
60
+ mode: 'http-gateway',
61
+ nativeProxyProtocol: true
89
62
  },
90
63
  activeChannel: sanitizeChannel(activeChannel),
91
64
  enabledChannelsCount: enabledChannels.length,
@@ -107,11 +80,10 @@ router.post('/start', async (req, res) => {
107
80
 
108
81
  let currentChannel = selectLatestEnabledChannel(enabledChannels) || enabledChannels[0];
109
82
  currentChannel = markChannelAsRecentlyUsed(currentChannel.id);
110
- saveActiveChannelId(currentChannel.id);
111
83
 
112
- const proxyResult = await startOmpProxyServer();
84
+ const proxyResult = await startOmpProxyServer({ activeChannelId: currentChannel.id });
113
85
  if (!proxyResult.success) {
114
- return res.status(500).json({ error: 'Failed to enable OMP models.yml provider config' });
86
+ return res.status(500).json({ error: 'Failed to start OMP gateway' });
115
87
  }
116
88
 
117
89
  const { broadcastProxyState, broadcastSchedulerState } = require('../websocket-server');
@@ -125,10 +97,10 @@ router.post('/start', async (req, res) => {
125
97
  success: true,
126
98
  port: proxyResult.port,
127
99
  activeChannel: sanitizeChannel(activeChannel),
128
- mode: 'models-yml-provider-config',
100
+ mode: 'http-gateway',
129
101
  sync: proxyResult.sync || null,
130
102
  warnings: proxyResult.warnings || [],
131
- message: `OMP models.yml provider config enabled, active channel: ${activeChannel.name}`
103
+ message: `OMP gateway and managed providers enabled, active channel: ${activeChannel.name}`
132
104
  });
133
105
  } catch (error) {
134
106
  console.error('[OMP Proxy] Start failed:', error);
@@ -139,7 +111,6 @@ router.post('/start', async (req, res) => {
139
111
  router.post('/stop', async (req, res) => {
140
112
  try {
141
113
  const result = await stopOmpProxyServer();
142
- removeActiveChannelFile();
143
114
  const { broadcastProxyState, broadcastSchedulerState } = require('../websocket-server');
144
115
  const { channels } = getChannels();
145
116
  broadcastProxyState('omp', getOmpProxyStatus(), null, channels);
@@ -465,13 +465,13 @@ function autoRestoreProxies() {
465
465
  startOmpProxyServer({ preserveStartTime: true })
466
466
  .then((result) => {
467
467
  if (result.success) {
468
- console.log(chalk.green(`[OK] OMP models.yml 受管 provider 已自动启用,端口: ${result.port}`));
468
+ console.log(chalk.green(`[OK] OMP 动态网关已自动恢复: http://127.0.0.1:${result.port}`));
469
469
  } else {
470
- console.error(chalk.red(`[ERROR] OMP models.yml 受管 provider 恢复失败: ${result.error || 'Unknown error'}`));
470
+ console.error(chalk.red(`[ERROR] OMP 动态网关恢复失败: ${result.error || 'Unknown error'}`));
471
471
  }
472
472
  })
473
473
  .catch((err) => {
474
- console.error(chalk.red(`[ERROR] OMP models.yml 受管 provider 恢复失败: ${err.message}`));
474
+ console.error(chalk.red(`[ERROR] OMP 动态网关恢复失败: ${err.message}`));
475
475
  });
476
476
  }
477
477
  }
@@ -1,17 +1,96 @@
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
+ getEnabledChannels,
8
+ isManagedOmpModeEnabled,
9
+ enableManagedOmpMode,
10
+ disableManagedOmpMode,
11
+ loadManagedOmpActiveChannelId,
12
+ loadManagedOmpModeState
13
+ } = require('./services/omp-channels');
14
+ const {
15
+ startOmpSessionLogObserver,
16
+ stopOmpSessionLogObserver,
17
+ getOmpSessionLogObserverStatus
18
+ } = require('./services/omp-session-log-observer');
19
+ const { createOmpGateway } = require('./services/omp-gateway');
20
+ const { prepareManagedOmpChannels } = require('./services/omp-gateway-routing');
21
+ const { probeOmpAuthGateways } = require('./services/omp-auth-gateway-client');
4
22
 
5
- let running = false;
6
23
  let currentPort = null;
7
24
  let lastSyncResult = null;
25
+ let lastRoutingResult = { routes: [], unsupportedChannels: [] };
26
+ let lifecycle = Promise.resolve();
8
27
 
9
- async function startOmpProxyServer(options = {}) {
28
+ const gateway = createOmpGateway({
29
+ getChannels: () => getEnabledChannels()
30
+ });
31
+
32
+ function withLifecycleLock(operation) {
33
+ const next = lifecycle.then(operation, operation);
34
+ lifecycle = next.catch(() => {});
35
+ return next;
36
+ }
37
+
38
+ async function startOmpProxyServerUnlocked(options = {}) {
39
+ if (gateway.status().listening) {
40
+ return {
41
+ success: true,
42
+ port: gateway.status().port,
43
+ sync: lastSyncResult,
44
+ warnings: lastSyncResult?.warnings || []
45
+ };
46
+ }
10
47
  const preserveStartTime = options.preserveStartTime || false;
11
48
  const config = loadConfig();
12
- currentPort = config.ports?.ompProxy || 20092;
13
- lastSyncResult = syncManagedOmpProviders();
14
- running = true;
49
+ const wasEnabled = isManagedOmpModeEnabled();
50
+ const previousState = loadManagedOmpModeState?.() || {
51
+ activeChannelId: loadManagedOmpActiveChannelId()
52
+ };
53
+ const channels = getEnabledChannels();
54
+ const secret = crypto.randomBytes(32).toString('hex');
55
+ const desiredPort = options.port ?? config.ports?.ompProxy ?? 20092;
56
+ const gatewayStatus = await gateway.start({
57
+ host: '127.0.0.1',
58
+ port: desiredPort,
59
+ secret
60
+ });
61
+ currentPort = gatewayStatus.port;
62
+ try {
63
+ const oauthSupport = await probeOmpAuthGateways(channels);
64
+ const descriptor = {
65
+ ...gateway.descriptor(),
66
+ supportedOAuthChannelIds: oauthSupport.supportedOAuthChannelIds
67
+ };
68
+ lastRoutingResult = prepareManagedOmpChannels(channels, descriptor);
69
+ enableManagedOmpMode(options.activeChannelId, descriptor);
70
+ lastSyncResult = syncManagedOmpProviders(channels, {
71
+ gateway: descriptor,
72
+ activeChannelId: options.activeChannelId
73
+ });
74
+ if (oauthSupport.warnings.length > 0) {
75
+ lastSyncResult = {
76
+ ...lastSyncResult,
77
+ warnings: [
78
+ ...(lastSyncResult?.warnings || []),
79
+ ...oauthSupport.warnings
80
+ ]
81
+ };
82
+ }
83
+ } catch (error) {
84
+ if (wasEnabled) {
85
+ enableManagedOmpMode(previousState.activeChannelId, previousState.gateway || null);
86
+ } else {
87
+ disableManagedOmpMode();
88
+ }
89
+ await gateway.stop().catch(() => {});
90
+ currentPort = null;
91
+ throw error;
92
+ }
93
+ startOmpSessionLogObserver();
15
94
  saveProxyStartTime('omp', preserveStartTime);
16
95
  return {
17
96
  success: true,
@@ -21,12 +100,34 @@ async function startOmpProxyServer(options = {}) {
21
100
  };
22
101
  }
23
102
 
24
- async function stopOmpProxyServer(options = {}) {
103
+ function startOmpProxyServer(options = {}) {
104
+ return withLifecycleLock(() => startOmpProxyServerUnlocked(options));
105
+ }
106
+
107
+ async function stopOmpProxyServerUnlocked(options = {}) {
25
108
  const clearStartTime = options.clearStartTime !== false;
26
109
  const stoppedPort = currentPort;
27
- lastSyncResult = disableManagedOmpProviders();
28
- running = false;
110
+ if (!gateway.status().listening && !isManagedOmpModeEnabled()) {
111
+ return {
112
+ success: true,
113
+ port: stoppedPort,
114
+ sync: lastSyncResult,
115
+ warnings: lastSyncResult?.warnings || []
116
+ };
117
+ }
118
+ gateway.beginDraining();
119
+ stopOmpSessionLogObserver();
120
+ try {
121
+ lastSyncResult = disableManagedOmpProviders();
122
+ } catch (error) {
123
+ startOmpSessionLogObserver();
124
+ gateway.cancelDraining();
125
+ throw error;
126
+ }
127
+ disableManagedOmpMode();
128
+ await gateway.stop({ forceAfterMs: options.forceAfterMs });
29
129
  currentPort = null;
130
+ lastRoutingResult = { routes: [], unsupportedChannels: [] };
30
131
  if (clearStartTime) {
31
132
  clearProxyStartTime('omp');
32
133
  }
@@ -38,8 +139,14 @@ async function stopOmpProxyServer(options = {}) {
38
139
  };
39
140
  }
40
141
 
142
+ function stopOmpProxyServer(options = {}) {
143
+ return withLifecycleLock(() => stopOmpProxyServerUnlocked(options));
144
+ }
145
+
41
146
  function getOmpProxyStatus() {
42
147
  const config = loadConfig();
148
+ const gatewayStatus = gateway.status();
149
+ const running = gatewayStatus.listening;
43
150
  const startTime = getProxyStartTime('omp', { allowRecovery: running });
44
151
  const runtime = getProxyRuntime('omp', { allowRecovery: running });
45
152
  const enabledCount = getEnabledChannels().length;
@@ -50,11 +157,23 @@ function getOmpProxyStatus() {
50
157
  defaultPort: config.ports?.ompProxy || 20092,
51
158
  startTime,
52
159
  runtime,
53
- mode: 'models-yml-provider-config',
160
+ mode: 'http-gateway',
161
+ listening: gatewayStatus.listening,
162
+ draining: gatewayStatus.draining,
163
+ inflightRequests: gatewayStatus.inflightRequests,
164
+ routingGroups: lastRoutingResult.routes.map(route => ({
165
+ providerKey: route.providerKey,
166
+ providerApi: route.providerApi,
167
+ routingGroup: route.routingGroup,
168
+ managedProviderId: route.managedProviderId,
169
+ channelsCount: route.channelIds.length
170
+ })),
171
+ unsupportedChannels: lastRoutingResult.unsupportedChannels,
54
172
  enabledChannelsCount: enabledCount,
55
173
  modelsPath: lastSyncResult?.modelsPath || lastSyncResult?.path || null,
56
174
  backupPath: lastSyncResult?.backupPath || null,
57
175
  modelsValidation: validation,
176
+ sessionLogObserver: getOmpSessionLogObserverStatus(),
58
177
  warnings: lastSyncResult?.warnings || []
59
178
  };
60
179
  }