coding-tool-x 3.7.10 → 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-CkUtnoz9.js → Analytics-Ck_00Brj.js} +1 -1
  2. package/dist/web/assets/{ConfigTemplates-BwPqhE-N.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-JMXWl37b.js → PluginManager-2RlvEJCT.js} +1 -1
  6. package/dist/web/assets/{ProjectList-CZwDgVvt.js → ProjectList-_ecCngpl.js} +1 -1
  7. package/dist/web/assets/{SessionList-2fZmN7Au.js → SessionList-DlWcw3hO.js} +1 -1
  8. package/dist/web/assets/{SkillManager-Bhzhkeis.js → SkillManager-CnY7IXkX.js} +1 -1
  9. package/dist/web/assets/{WorkspaceManager-B3lhhBD0.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 +6 -4
  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 +335 -86
  31. package/dist/web/assets/Home-BANl91zG.js +0 -1
  32. package/dist/web/assets/Home-CqnBXFxj.css +0 -1
  33. package/dist/web/assets/index-LYvv7wtx.js +0 -2
@@ -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,82 @@ const {
23
22
 
24
23
  const OMP_THINKING_SUFFIX_RE = /:(minimal|low|medium|high|xhigh|off)$/;
25
24
 
25
+ function loadManagedOmpModeState() {
26
+ ensureStorageDirMigrated();
27
+ try {
28
+ if (!fs.existsSync(PATHS.activeChannel.omp)) return null;
29
+ const data = JSON.parse(fs.readFileSync(PATHS.activeChannel.omp, 'utf8'));
30
+ return data && typeof data === 'object' && !Array.isArray(data) ? data : null;
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ function loadManagedOmpActiveChannelId() {
37
+ return loadManagedOmpModeState()?.activeChannelId || null;
38
+ }
39
+
40
+ function isManagedOmpModeEnabled() {
41
+ ensureStorageDirMigrated();
42
+ return fs.existsSync(PATHS.activeChannel.omp);
43
+ }
44
+
45
+ function writeManagedOmpModeState(filePath, state) {
46
+ const temporaryPath = `${filePath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
47
+ try {
48
+ fs.writeFileSync(temporaryPath, JSON.stringify(state, null, 2), {
49
+ encoding: 'utf8',
50
+ mode: 0o600
51
+ });
52
+ fs.renameSync(temporaryPath, filePath);
53
+ } catch (error) {
54
+ if (fs.existsSync(temporaryPath)) fs.unlinkSync(temporaryPath);
55
+ throw error;
56
+ }
57
+ }
58
+
59
+ function enableManagedOmpMode(activeChannelId = null, gateway = null) {
60
+ ensureStorageDirMigrated();
61
+ const filePath = PATHS.activeChannel.omp;
62
+ const dir = path.dirname(filePath);
63
+ if (!fs.existsSync(dir)) {
64
+ fs.mkdirSync(dir, { recursive: true });
65
+ }
66
+ const nextActiveChannelId = activeChannelId || loadManagedOmpActiveChannelId();
67
+ const previousState = loadManagedOmpModeState();
68
+ const nextGateway = gateway || previousState?.gateway || null;
69
+ const state = {
70
+ version: 2,
71
+ activeChannelId: nextActiveChannelId || null
72
+ };
73
+ if (nextGateway) {
74
+ state.gateway = {
75
+ host: String(nextGateway.host || '127.0.0.1'),
76
+ port: Number(nextGateway.port),
77
+ secret: String(nextGateway.secret || ''),
78
+ supportedOAuthChannelIds: Array.isArray(nextGateway.supportedOAuthChannelIds)
79
+ ? [...nextGateway.supportedOAuthChannelIds]
80
+ : []
81
+ };
82
+ }
83
+ writeManagedOmpModeState(filePath, state);
84
+ return nextActiveChannelId || null;
85
+ }
86
+
87
+ function disableManagedOmpMode() {
88
+ ensureStorageDirMigrated();
89
+ if (fs.existsSync(PATHS.activeChannel.omp)) {
90
+ fs.unlinkSync(PATHS.activeChannel.omp);
91
+ }
92
+ }
93
+
26
94
  class OmpChannelService extends BaseChannelService {
27
95
  constructor() {
28
96
  super({
29
97
  platform: 'omp',
30
98
  channelsFilePath: PATHS.channels.omp,
31
99
  defaultGatewaySource: 'openai_compatible',
32
- isProxyRunning: () => isManagedOmpProvidersActive()
100
+ isProxyRunning: () => isManagedOmpModeEnabled()
33
101
  });
34
102
  }
35
103
 
@@ -37,10 +105,16 @@ class OmpChannelService extends BaseChannelService {
37
105
  const normalized = super._applyDefaults(channel);
38
106
  normalized.providerKey = normalized.providerKey || normalized.provider || normalized.name || normalized.id;
39
107
  normalized.providerApi = normalized.providerApi || normalized.api || normalized.wireApi || 'openai-completions';
40
- normalized.authMode = normalized.authMode === 'oauth' ? 'oauth' : 'api_key';
108
+ normalized.authMode = ['api_key', 'oauth', 'none'].includes(normalized.authMode)
109
+ ? normalized.authMode
110
+ : 'api_key';
111
+ normalized.routingGroup = String(normalized.routingGroup || '').trim();
41
112
  normalized.oauthProviderId = normalized.oauthProviderId || (normalized.authMode === 'oauth' ? normalized.providerKey : '');
42
113
  if (normalized.authMode === 'oauth') {
43
114
  normalized.apiKey = normalized.apiKey || '';
115
+ } else if (normalized.authMode === 'none') {
116
+ normalized.apiKey = '';
117
+ normalized.oauthProviderId = '';
44
118
  }
45
119
  normalized.model = normalized.model || null;
46
120
  normalized.models = Array.isArray(normalized.models) ? normalized.models : [];
@@ -59,28 +133,46 @@ class OmpChannelService extends BaseChannelService {
59
133
  }
60
134
 
61
135
  _applyToNativeSettings(channel) {
62
- writeManagedOmpProviders([channel]);
136
+ if (isManagedOmpModeEnabled()) {
137
+ const state = loadManagedOmpModeState();
138
+ enableManagedOmpMode(channel.id, state?.gateway || null);
139
+ writeManagedOmpProviders([channel], state?.gateway ? {
140
+ gateway: state.gateway,
141
+ activeChannelId: channel.id
142
+ } : {});
143
+ }
63
144
  }
64
145
 
65
146
  _onAfterCreate(_channel, allChannels) {
66
- this.syncManagedOmpProviders(allChannels);
147
+ this.syncManagedOmpProvidersIfEnabled(allChannels);
67
148
  }
68
149
 
69
150
  _onAfterUpdate(_oldChannel, _newChannel, allChannels) {
70
- this.syncManagedOmpProviders(allChannels);
151
+ this.syncManagedOmpProvidersIfEnabled(allChannels);
71
152
  }
72
153
 
73
154
  _onAfterDelete(_channel, allChannels) {
74
- this.syncManagedOmpProviders(allChannels);
155
+ this.syncManagedOmpProvidersIfEnabled(allChannels);
156
+ }
157
+
158
+ syncManagedOmpProvidersIfEnabled(channels = this.getChannels().channels) {
159
+ if (!isManagedOmpModeEnabled()) {
160
+ return getLastManagedOmpSyncResult();
161
+ }
162
+ const state = loadManagedOmpModeState();
163
+ return this.syncManagedOmpProviders(channels, state?.gateway ? {
164
+ gateway: state.gateway,
165
+ activeChannelId: state.activeChannelId
166
+ } : {});
75
167
  }
76
168
 
77
- syncManagedOmpProviders(channels = this.getChannels().channels) {
169
+ syncManagedOmpProviders(channels = this.getChannels().channels, options = {}) {
78
170
  const enabledChannels = (channels || []).filter(channel => channel.enabled !== false);
79
171
  if (enabledChannels.length === 0) {
80
172
  removeManagedOmpProviders();
81
173
  return getLastManagedOmpSyncResult();
82
174
  }
83
- writeManagedOmpProviders(enabledChannels);
175
+ writeManagedOmpProviders(enabledChannels, options);
84
176
  return getLastManagedOmpSyncResult();
85
177
  }
86
178
 
@@ -90,7 +182,7 @@ class OmpChannelService extends BaseChannelService {
90
182
  }
91
183
 
92
184
  syncManagedProviderExtension(channels = this.getChannels().channels) {
93
- return this.syncManagedOmpProviders(channels);
185
+ return this.syncManagedOmpProvidersIfEnabled(channels);
94
186
  }
95
187
 
96
188
  disableManagedProviderExtension() {
@@ -396,9 +488,14 @@ module.exports = {
396
488
  applyChannelToSettings: (id) => service.applyChannelToSettings(id),
397
489
  disableAllChannels: () => service.disableAllChannels(),
398
490
  getEffectiveApiKey: (channel) => channel?.apiKey || null,
399
- syncManagedOmpProviders: (channels) => service.syncManagedOmpProviders(channels),
491
+ syncManagedOmpProviders: (channels, options) => service.syncManagedOmpProviders(channels, options),
400
492
  disableManagedOmpProviders: () => service.disableManagedOmpProviders(),
401
493
  syncManagedProviderExtension: (channels) => service.syncManagedProviderExtension(channels),
402
494
  disableManagedProviderExtension: () => service.disableManagedProviderExtension(),
495
+ isManagedOmpModeEnabled,
496
+ enableManagedOmpMode,
497
+ disableManagedOmpMode,
498
+ loadManagedOmpModeState,
499
+ loadManagedOmpActiveChannelId,
403
500
  syncCurrentOmpChannel
404
501
  };
@@ -0,0 +1,295 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ const MANAGED_PREFIX = 'ctx-';
6
+ const SUPPORTED_PROVIDER_APIS = new Set([
7
+ 'openai-completions',
8
+ 'openai-responses',
9
+ 'openai-codex-responses',
10
+ 'azure-openai-responses',
11
+ 'anthropic-messages',
12
+ 'google-generative-ai',
13
+ 'google-gemini-cli',
14
+ 'google-vertex'
15
+ ]);
16
+
17
+ function normalizeText(value) {
18
+ return String(value || '').trim();
19
+ }
20
+
21
+ function normalizeProviderId(value) {
22
+ return normalizeText(value)
23
+ .toLowerCase()
24
+ .replace(/[^a-z0-9._-]+/g, '-')
25
+ .replace(/^-+|-+$/g, '') || 'provider';
26
+ }
27
+
28
+ function getProviderKey(channel = {}) {
29
+ return normalizeText(channel.providerKey || channel.provider || channel.name || channel.id);
30
+ }
31
+
32
+ function getProviderApi(channel = {}) {
33
+ return normalizeText(channel.providerApi || channel.api || channel.wireApi || 'openai-completions');
34
+ }
35
+
36
+ function getRoutingGroup(channel = {}) {
37
+ return normalizeText(channel.routingGroup) || normalizeText(channel.id);
38
+ }
39
+
40
+ function stableToken(secret, namespace, value, length = 24) {
41
+ return crypto
42
+ .createHmac('sha256', String(secret || ''))
43
+ .update(`${namespace}\0${value}`)
44
+ .digest('hex')
45
+ .slice(0, length);
46
+ }
47
+
48
+ function stablePublicId(namespace, value, length = 8) {
49
+ return crypto
50
+ .createHash('sha256')
51
+ .update(`${namespace}\0${value}`)
52
+ .digest('hex')
53
+ .slice(0, length);
54
+ }
55
+
56
+ function normalizeRedirects(channel = {}) {
57
+ const redirects = Array.isArray(channel.modelRedirects) ? channel.modelRedirects : [];
58
+ return JSON.stringify(redirects);
59
+ }
60
+
61
+ function canonicalize(value) {
62
+ if (Array.isArray(value)) return value.map(canonicalize);
63
+ if (value && typeof value === 'object') {
64
+ return Object.fromEntries(
65
+ Object.keys(value).sort().map(key => [key, canonicalize(value[key])])
66
+ );
67
+ }
68
+ return value;
69
+ }
70
+
71
+ function getModelCapabilityMap(channel = {}) {
72
+ const result = new Map();
73
+ const add = (raw) => {
74
+ const model = typeof raw === 'string' ? { id: raw } : raw;
75
+ const id = normalizeText(model?.id || model?.name);
76
+ if (!id || result.has(id)) return;
77
+ const capability = { ...(model || {}) };
78
+ delete capability.id;
79
+ delete capability.name;
80
+ delete capability.baseUrl;
81
+ delete capability.headers;
82
+ result.set(id, JSON.stringify(canonicalize(capability)));
83
+ };
84
+ (Array.isArray(channel.models) ? channel.models : []).forEach(add);
85
+ (Array.isArray(channel.allowedModels) ? channel.allowedModels : []).forEach(add);
86
+ add(channel.model);
87
+ return result;
88
+ }
89
+
90
+ function getGroupKey(channel = {}) {
91
+ return [
92
+ getProviderKey(channel),
93
+ getProviderApi(channel),
94
+ getRoutingGroup(channel)
95
+ ].join('\0');
96
+ }
97
+
98
+ function assertCompatibleGroup(channels = []) {
99
+ if (channels.length <= 1) return;
100
+ const first = channels[0];
101
+ const expectedApi = getProviderApi(first);
102
+ const expectedProvider = getProviderKey(first);
103
+ const expectedRedirects = normalizeRedirects(first);
104
+ const expectedAuthMode = normalizeText(first.authMode) || 'api_key';
105
+ const expectedCapabilities = getModelCapabilityMap(first);
106
+
107
+ channels.slice(1).forEach((channel) => {
108
+ if (getProviderKey(channel) !== expectedProvider) {
109
+ throw new Error(`OMP routing group "${getRoutingGroup(first)}" mixes providerKey values`);
110
+ }
111
+ if (getProviderApi(channel) !== expectedApi) {
112
+ throw new Error(`OMP routing group "${getRoutingGroup(first)}" mixes providerApi values`);
113
+ }
114
+ if (normalizeRedirects(channel) !== expectedRedirects) {
115
+ throw new Error(`OMP routing group "${getRoutingGroup(first)}" has inconsistent modelRedirects`);
116
+ }
117
+ if ((normalizeText(channel.authMode) || 'api_key') !== expectedAuthMode) {
118
+ throw new Error(`OMP routing group "${getRoutingGroup(first)}" mixes authMode values`);
119
+ }
120
+ const actualCapabilities = getModelCapabilityMap(channel);
121
+ expectedCapabilities.forEach((capability, modelId) => {
122
+ if (actualCapabilities.has(modelId) && actualCapabilities.get(modelId) !== capability) {
123
+ throw new Error(
124
+ `OMP routing group "${getRoutingGroup(first)}" has incompatible capability metadata for model "${modelId}"`
125
+ );
126
+ }
127
+ });
128
+ });
129
+ }
130
+
131
+ function sanitizeProviderConfig(channel = {}) {
132
+ const source = channel.providerConfig && typeof channel.providerConfig === 'object'
133
+ && !Array.isArray(channel.providerConfig)
134
+ ? channel.providerConfig
135
+ : {};
136
+ const next = { ...source };
137
+ for (const key of ['baseUrl', 'base_url', 'apiKey', 'api_key', 'headers', 'auth']) {
138
+ delete next[key];
139
+ }
140
+ return next;
141
+ }
142
+
143
+ function sanitizeModels(channel = {}) {
144
+ return (Array.isArray(channel.models) ? channel.models : []).map((model) => {
145
+ if (!model || typeof model !== 'object' || Array.isArray(model)) return model;
146
+ const next = { ...model };
147
+ delete next.baseUrl;
148
+ delete next.headers;
149
+ return next;
150
+ });
151
+ }
152
+
153
+ function mergeGroupField(channels = [], field) {
154
+ const seen = new Set();
155
+ const result = [];
156
+ channels.forEach((channel) => {
157
+ const values = Array.isArray(channel?.[field]) ? channel[field] : [];
158
+ values.forEach((value) => {
159
+ const id = normalizeText(typeof value === 'string' ? value : (value?.id || value?.name));
160
+ if (!id || seen.has(id.toLowerCase())) return;
161
+ seen.add(id.toLowerCase());
162
+ result.push(value);
163
+ });
164
+ });
165
+ return result;
166
+ }
167
+
168
+ function buildManagedBaseUrl(host, port, routeToken) {
169
+ return `http://${host}:${port}/omp/${routeToken}`;
170
+ }
171
+
172
+ function prepareManagedOmpChannels(channels = [], gateway = {}) {
173
+ const host = normalizeText(gateway.host) || '127.0.0.1';
174
+ const port = Number(gateway.port);
175
+ const secret = normalizeText(gateway.secret);
176
+ if (!Number.isInteger(port) || port <= 0) {
177
+ throw new Error('OMP gateway port is required before managed providers can be generated');
178
+ }
179
+ if (!secret) {
180
+ throw new Error('OMP gateway secret is required before managed providers can be generated');
181
+ }
182
+
183
+ const enabled = channels.filter(channel => channel && channel.enabled !== false && channel.baseUrl);
184
+ const groups = new Map();
185
+ enabled.forEach((channel) => {
186
+ const key = getGroupKey(channel);
187
+ if (!groups.has(key)) groups.set(key, []);
188
+ groups.get(key).push(channel);
189
+ });
190
+
191
+ const providerCounts = new Map();
192
+ groups.forEach((groupChannels) => {
193
+ const providerId = normalizeProviderId(getProviderKey(groupChannels[0]));
194
+ providerCounts.set(providerId, (providerCounts.get(providerId) || 0) + 1);
195
+ });
196
+
197
+ const routes = [];
198
+ const managedChannels = [];
199
+ const unsupportedChannels = [];
200
+
201
+ groups.forEach((groupChannels, groupKey) => {
202
+ assertCompatibleGroup(groupChannels);
203
+ const template = groupChannels[0];
204
+ if (!SUPPORTED_PROVIDER_APIS.has(getProviderApi(template))) {
205
+ groupChannels.forEach(channel => unsupportedChannels.push({
206
+ id: channel.id,
207
+ name: channel.name,
208
+ reason: 'unsupported-provider-api'
209
+ }));
210
+ return;
211
+ }
212
+ const authMode = normalizeText(template.authMode) || 'api_key';
213
+ const transport = normalizeText(template.transport || template.providerConfig?.transport);
214
+ const supportedOAuthIds = Array.isArray(gateway.supportedOAuthChannelIds)
215
+ ? new Set(gateway.supportedOAuthChannelIds)
216
+ : null;
217
+ const oauthGatewayUnavailable = authMode === 'oauth' && (
218
+ !normalizeText(template.apiKey)
219
+ || transport !== 'pi-native'
220
+ || (supportedOAuthIds && groupChannels.some(channel => !supportedOAuthIds.has(channel.id)))
221
+ );
222
+ if (oauthGatewayUnavailable) {
223
+ groupChannels.forEach(channel => unsupportedChannels.push({
224
+ id: channel.id,
225
+ name: channel.name,
226
+ reason: 'oauth-auth-gateway-unavailable'
227
+ }));
228
+ return;
229
+ }
230
+
231
+ const providerId = normalizeProviderId(getProviderKey(template));
232
+ const routeToken = stableToken(secret, 'route', groupKey);
233
+ const capability = `ctx_${stableToken(secret, 'capability', groupKey, 40)}`;
234
+ const multipleGroups = providerCounts.get(providerId) > 1;
235
+ const managedProviderId = multipleGroups
236
+ ? `${MANAGED_PREFIX}${providerId}-${stablePublicId('provider', groupKey)}`
237
+ : `${MANAGED_PREFIX}${providerId}`;
238
+ const routingGroup = getRoutingGroup(template);
239
+ const mergedModels = sanitizeModels({
240
+ models: mergeGroupField(groupChannels, 'models')
241
+ });
242
+ const mergedAllowedModels = mergeGroupField(groupChannels, 'allowedModels');
243
+
244
+ const route = {
245
+ token: routeToken,
246
+ capability,
247
+ routingGroup,
248
+ providerKey: getProviderKey(template),
249
+ providerApi: getProviderApi(template),
250
+ managedProviderId,
251
+ channelIds: groupChannels.map(channel => channel.id),
252
+ authMode
253
+ };
254
+ routes.push(route);
255
+
256
+ managedChannels.push({
257
+ ...template,
258
+ id: `omp-route-${routeToken}`,
259
+ managedProviderId,
260
+ originalProviderId: getProviderKey(template),
261
+ routingGroup,
262
+ baseUrl: buildManagedBaseUrl(host, port, routeToken),
263
+ apiKey: authMode === 'none' ? '' : capability,
264
+ authMode,
265
+ providerConfig: sanitizeProviderConfig(template),
266
+ headers: undefined,
267
+ auth: authMode === 'none' ? 'none' : undefined,
268
+ models: mergedModels,
269
+ allowedModels: mergedAllowedModels,
270
+ _ompGatewayRoute: route
271
+ });
272
+ });
273
+
274
+ return { routes, managedChannels, unsupportedChannels };
275
+ }
276
+
277
+ function resolveOmpGatewayRoute(pathname, channels, gateway) {
278
+ const match = String(pathname || '').match(/^\/omp\/([a-f0-9]{24})(\/.*|$)/);
279
+ if (!match) return null;
280
+ const prepared = prepareManagedOmpChannels(channels, gateway);
281
+ const route = prepared.routes.find(item => item.token === match[1]);
282
+ if (!route) return null;
283
+ return {
284
+ ...route,
285
+ upstreamPath: match[2] || '/'
286
+ };
287
+ }
288
+
289
+ module.exports = {
290
+ prepareManagedOmpChannels,
291
+ resolveOmpGatewayRoute,
292
+ getRoutingGroup,
293
+ getProviderKey,
294
+ getProviderApi
295
+ };