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.
- package/dist/web/assets/{Analytics-CkUtnoz9.js → Analytics-Ck_00Brj.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-BwPqhE-N.js → ConfigTemplates-Cpw2vUa8.js} +1 -1
- package/dist/web/assets/Home-Bft3Kztl.js +1 -0
- package/dist/web/assets/Home-CHZJkkjH.css +1 -0
- package/dist/web/assets/{PluginManager-JMXWl37b.js → PluginManager-2RlvEJCT.js} +1 -1
- package/dist/web/assets/{ProjectList-CZwDgVvt.js → ProjectList-_ecCngpl.js} +1 -1
- package/dist/web/assets/{SessionList-2fZmN7Au.js → SessionList-DlWcw3hO.js} +1 -1
- package/dist/web/assets/{SkillManager-Bhzhkeis.js → SkillManager-CnY7IXkX.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-B3lhhBD0.js → WorkspaceManager-3mqghi09.js} +1 -1
- package/dist/web/assets/{index-C5nLTbOk.css → index-BceXgSSe.css} +1 -1
- package/dist/web/assets/index-k5oP7c-A.js +2 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/commands/toggle-proxy.js +7 -8
- package/src/commands/ui.js +11 -1
- package/src/config/default.js +1 -1
- package/src/index.js +41 -15
- package/src/server/api/omp-channels.js +6 -4
- package/src/server/api/omp-config.js +1 -1
- package/src/server/api/omp-proxy.js +13 -42
- package/src/server/index.js +3 -3
- package/src/server/omp-proxy-server.js +129 -10
- package/src/server/services/channel-scheduler.js +59 -9
- package/src/server/services/omp-auth-gateway-client.js +62 -0
- package/src/server/services/omp-channels.js +109 -12
- package/src/server/services/omp-gateway-routing.js +295 -0
- package/src/server/services/omp-gateway.js +919 -0
- package/src/server/services/omp-session-log-observer.js +138 -0
- package/src/server/services/omp-sessions.js +80 -0
- package/src/server/services/omp-settings-manager.js +335 -86
- package/dist/web/assets/Home-BANl91zG.js +0 -1
- package/dist/web/assets/Home-CqnBXFxj.css +0 -1
- 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: () =>
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
147
|
+
this.syncManagedOmpProvidersIfEnabled(allChannels);
|
|
67
148
|
}
|
|
68
149
|
|
|
69
150
|
_onAfterUpdate(_oldChannel, _newChannel, allChannels) {
|
|
70
|
-
this.
|
|
151
|
+
this.syncManagedOmpProvidersIfEnabled(allChannels);
|
|
71
152
|
}
|
|
72
153
|
|
|
73
154
|
_onAfterDelete(_channel, allChannels) {
|
|
74
|
-
this.
|
|
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.
|
|
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
|
+
};
|