coding-tool-x 3.5.13 → 3.6.0

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.
@@ -13,12 +13,14 @@ const {
13
13
  } = require('../services/channels');
14
14
  const { getSchedulerState } = require('../services/channel-scheduler');
15
15
  const { getChannelHealthStatus, getAllChannelHealthStatus, resetChannelHealth } = require('../services/channel-health');
16
+ const { fetchModelsFromProvider } = require('../services/model-detector');
16
17
  const {
17
18
  testChannelSpeed,
18
19
  getLatencyLevel,
19
20
  sanitizeBatchConcurrency,
20
21
  runWithConcurrencyLimit
21
22
  } = require('../services/speed-test');
23
+ const { normalizeGatewaySourceType } = require('../services/base/proxy-utils');
22
24
  const { PATHS } = require('../../config/paths');
23
25
  const { deleteBackup } = require('../services/settings-manager');
24
26
  const { getDefaultSpeedTestModelByToolType } = require('../../config/model-metadata');
@@ -30,6 +32,19 @@ function getDefaultClaudeModel() {
30
32
  return getDefaultSpeedTestModelByToolType('claude');
31
33
  }
32
34
 
35
+ function resolveClaudeGatewaySourceType(channel, requestedType) {
36
+ return normalizeGatewaySourceType(requestedType || channel?.gatewaySourceType, 'claude');
37
+ }
38
+
39
+ function getDefaultModelsForGatewaySourceType(gatewaySourceType) {
40
+ if (gatewaySourceType === 'openai_compatible' || gatewaySourceType === 'codex') {
41
+ const model = getDefaultSpeedTestModelByToolType('codex');
42
+ return model ? [model] : [];
43
+ }
44
+ const model = getDefaultClaudeModel();
45
+ return model ? [model] : [];
46
+ }
47
+
33
48
  // GET /api/channels - Get all channels with health status
34
49
  router.get('/', (req, res) => {
35
50
  try {
@@ -92,7 +107,8 @@ router.post('/', (req, res) => {
92
107
  modelRedirects,
93
108
  proxyUrl,
94
109
  speedTestModel,
95
- gatewaySourceType
110
+ gatewaySourceType,
111
+ targetApi
96
112
  } = req.body;
97
113
 
98
114
  if (!name || !baseUrl) {
@@ -112,7 +128,8 @@ router.post('/', (req, res) => {
112
128
  modelRedirects: modelRedirects || [],
113
129
  proxyUrl: proxyUrl || '',
114
130
  speedTestModel: speedTestModel || null,
115
- gatewaySourceType
131
+ gatewaySourceType,
132
+ targetApi
116
133
  });
117
134
  res.json({ channel });
118
135
  broadcastSchedulerState('claude', getSchedulerState('claude'));
@@ -224,7 +241,7 @@ router.post('/:id/apply-to-settings', async (req, res) => {
224
241
  });
225
242
  } catch (error) {
226
243
  console.error('Error applying channel to settings:', error);
227
- res.status(500).json({ error: error.message });
244
+ res.status(error.statusCode || 500).json({ error: error.message });
228
245
  }
229
246
  });
230
247
 
@@ -256,7 +273,7 @@ router.post('/:id/speed-test', async (req, res) => {
256
273
  return res.status(404).json({ error: '渠道不存在' });
257
274
  }
258
275
 
259
- const speedTestType = CLAUDE_GATEWAY_SOURCE_TYPE;
276
+ const speedTestType = resolveClaudeGatewaySourceType(channel);
260
277
  const result = await testChannelSpeed(channel, timeout, speedTestType);
261
278
  result.level = getLatencyLevel(result.latency);
262
279
  result.gatewaySourceType = speedTestType;
@@ -280,18 +297,46 @@ router.get('/:id/models', async (req, res) => {
280
297
  return res.status(404).json({ error: '渠道不存在' });
281
298
  }
282
299
 
283
- const gatewaySourceType = CLAUDE_GATEWAY_SOURCE_TYPE;
284
- const models = [getDefaultClaudeModel()];
285
- const now = new Date().toISOString();
286
- const result = {
287
- models,
288
- supported: models.length > 0,
289
- cached: false,
290
- fallbackUsed: false,
291
- lastChecked: now,
292
- error: models.length > 0 ? null : '未配置默认模型列表',
293
- errorHint: models.length > 0 ? null : '请在设置中配置 Claude 默认模型'
294
- };
300
+ const gatewaySourceType = resolveClaudeGatewaySourceType(channel, req.query.type);
301
+ const fallbackModels = getDefaultModelsForGatewaySourceType(gatewaySourceType);
302
+ let result;
303
+
304
+ if (gatewaySourceType === 'openai_compatible' || gatewaySourceType === 'codex') {
305
+ try {
306
+ result = await fetchModelsFromProvider(channel, gatewaySourceType);
307
+ } catch (error) {
308
+ result = {
309
+ models: [],
310
+ supported: true,
311
+ cached: false,
312
+ fallbackUsed: true,
313
+ lastChecked: new Date().toISOString(),
314
+ error: error.message || '获取模型列表失败',
315
+ errorHint: '已回退到默认模型列表'
316
+ };
317
+ }
318
+
319
+ if (!Array.isArray(result.models) || result.models.length === 0) {
320
+ result = {
321
+ ...result,
322
+ models: fallbackModels,
323
+ fallbackUsed: true,
324
+ error: result.error || (fallbackModels.length > 0 ? null : '未配置默认模型列表'),
325
+ errorHint: result.errorHint || (fallbackModels.length > 0 ? '已回退到默认模型列表' : '请在设置中配置默认模型')
326
+ };
327
+ }
328
+ } else {
329
+ const now = new Date().toISOString();
330
+ result = {
331
+ models: fallbackModels,
332
+ supported: fallbackModels.length > 0,
333
+ cached: false,
334
+ fallbackUsed: false,
335
+ lastChecked: now,
336
+ error: fallbackModels.length > 0 ? null : '未配置默认模型列表',
337
+ errorHint: fallbackModels.length > 0 ? null : '请在设置中配置 Claude 默认模型'
338
+ };
339
+ }
295
340
 
296
341
  res.json({
297
342
  channelId: id,
@@ -328,7 +373,7 @@ router.post('/speed-test-all', async (req, res) => {
328
373
  channels,
329
374
  safeConcurrency,
330
375
  async channel => {
331
- const speedTestType = CLAUDE_GATEWAY_SOURCE_TYPE;
376
+ const speedTestType = resolveClaudeGatewaySourceType(channel);
332
377
  const result = await testChannelSpeed(channel, timeout, speedTestType);
333
378
  result.level = getLatencyLevel(result.latency);
334
379
  result.gatewaySourceType = speedTestType;
@@ -166,7 +166,8 @@ module.exports = (config) => {
166
166
  modelRedirects: modelRedirects || [],
167
167
  speedTestModel: speedTestModel || null,
168
168
  presetId: presetId || null,
169
- gatewaySourceType
169
+ gatewaySourceType,
170
+ requiresOpenaiAuth: false
170
171
  });
171
172
  res.json(channel);
172
173
  broadcastSchedulerState('codex', getSchedulerState('codex'));
@@ -188,12 +189,22 @@ module.exports = (config) => {
188
189
 
189
190
  const { channelId } = req.params;
190
191
  const updates = req.body;
192
+ const currentChannel = getChannels().channels.find(channel => channel.id === channelId) || null;
193
+ const finalApiKey = Object.prototype.hasOwnProperty.call(updates, 'apiKey')
194
+ ? updates.apiKey
195
+ : currentChannel?.apiKey;
191
196
  if (Object.prototype.hasOwnProperty.call(updates, 'providerKey')) {
192
197
  const providerKeyError = validateCodexProviderKey(updates.providerKey);
193
198
  if (providerKeyError) {
194
199
  return res.status(400).json({ error: providerKeyError });
195
200
  }
196
201
  }
202
+ if (currentChannel) {
203
+ if (!finalApiKey) {
204
+ return res.status(400).json({ error: 'Missing required fields: apiKey' });
205
+ }
206
+ updates.requiresOpenaiAuth = false;
207
+ }
197
208
 
198
209
  const channel = updateChannel(channelId, updates);
199
210
  // 清除该渠道的模型重定向日志缓存,使下次请求时重新打印
@@ -1,12 +1,10 @@
1
1
  const express = require('express');
2
2
  const { exec } = require('child_process');
3
- const { promisify } = require('util');
4
3
  const path = require('path');
5
4
  const fs = require('fs');
6
5
  const pm2 = require('pm2');
7
6
  const { HOME_DIR } = require('../../config/paths');
8
-
9
- const execAsync = promisify(exec);
7
+ const PM2_BIN_PATH = require.resolve('pm2/bin/pm2');
10
8
 
11
9
  function getExecOptions(timeout = 30000, runtimePlatform = process.platform) {
12
10
  if (runtimePlatform === 'win32') {
@@ -15,6 +13,46 @@ function getExecOptions(timeout = 30000, runtimePlatform = process.platform) {
15
13
  return { shell: '/bin/bash', timeout, windowsHide: true };
16
14
  }
17
15
 
16
+ function shellQuote(value) {
17
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
18
+ }
19
+
20
+ function getPm2CliCommand() {
21
+ return `${shellQuote(process.execPath)} ${shellQuote(PM2_BIN_PATH)}`;
22
+ }
23
+
24
+ function listMatchingFiles(dirPath, matcher) {
25
+ if (!fs.existsSync(dirPath)) {
26
+ return [];
27
+ }
28
+
29
+ return fs.readdirSync(dirPath)
30
+ .filter(fileName => matcher.test(fileName))
31
+ .map(fileName => path.join(dirPath, fileName));
32
+ }
33
+
34
+ function getStartupCommand(runtimePlatform = process.platform) {
35
+ const pm2Cli = getPm2CliCommand();
36
+ if (runtimePlatform === 'darwin') {
37
+ return `${pm2Cli} startup launchd -u $(whoami) --hp $(eval echo ~$(whoami))`;
38
+ }
39
+ if (runtimePlatform === 'linux') {
40
+ return `${pm2Cli} startup systemd -u $(whoami) --hp $(eval echo ~$(whoami))`;
41
+ }
42
+ return `${pm2Cli} startup`;
43
+ }
44
+
45
+ function getUnstartupCommand(runtimePlatform = process.platform) {
46
+ const pm2Cli = getPm2CliCommand();
47
+ if (runtimePlatform === 'darwin') {
48
+ return `${pm2Cli} unstartup launchd -u $(whoami)`;
49
+ }
50
+ if (runtimePlatform === 'linux') {
51
+ return `${pm2Cli} unstartup systemd -u $(whoami)`;
52
+ }
53
+ return `${pm2Cli} unstartup`;
54
+ }
55
+
18
56
  /**
19
57
  * Check if PM2 autostart is enabled
20
58
  * by looking for PM2 startup script in system
@@ -24,17 +62,21 @@ async function checkAutoStartStatus() {
24
62
  const platform = process.platform;
25
63
 
26
64
  if (platform === 'darwin') {
27
- // macOS - check for LaunchDaemon
28
- const launchDaemonsPath = path.join(HOME_DIR, 'Library/LaunchDaemons');
29
- const pm2Files = fs.existsSync(launchDaemonsPath)
30
- ? fs.readdirSync(launchDaemonsPath).filter(f => f.includes('pm2'))
31
- : [];
32
-
33
- return { enabled: pm2Files.length > 0, platform: 'darwin' };
65
+ const pm2Files = [
66
+ ...listMatchingFiles(path.join(HOME_DIR, 'Library/LaunchAgents'), /pm2/i),
67
+ ...listMatchingFiles(path.join(HOME_DIR, 'Library/LaunchDaemons'), /pm2/i),
68
+ ...listMatchingFiles('/Library/LaunchAgents', /pm2/i),
69
+ ...listMatchingFiles('/Library/LaunchDaemons', /pm2/i)
70
+ ];
71
+
72
+ return {
73
+ enabled: pm2Files.length > 0,
74
+ platform: 'darwin',
75
+ files: pm2Files
76
+ };
34
77
  } else if (platform === 'linux') {
35
78
  // Linux - check for systemd service
36
79
  const systemdPath = '/etc/systemd/system/pm2-root.service';
37
- const userSystemdPath = path.join(HOME_DIR, '.config/systemd/user/pm2-*.service');
38
80
 
39
81
  const rootExists = fs.existsSync(systemdPath);
40
82
  const userExists = fs.existsSync(path.join(HOME_DIR, '.config/systemd/user')) &&
@@ -102,12 +144,7 @@ async function enableAutoStart() {
102
144
  }
103
145
 
104
146
  // Run startup command
105
- const platform = process.platform;
106
- const command = platform === 'darwin'
107
- ? 'pm2 startup launchd -u $(whoami) --hp $(eval echo ~$(whoami))'
108
- : platform === 'linux'
109
- ? 'pm2 startup systemd -u $(whoami) --hp $(eval echo ~$(whoami))'
110
- : 'pm2 startup';
147
+ const command = getStartupCommand();
111
148
 
112
149
  console.log(`Running startup command: ${command}`);
113
150
 
@@ -161,12 +198,7 @@ async function disableAutoStart() {
161
198
  }
162
199
 
163
200
  // Run unstartup command
164
- const platform = process.platform;
165
- const command = platform === 'darwin'
166
- ? 'pm2 unstartup launchd -u $(whoami)'
167
- : platform === 'linux'
168
- ? 'pm2 unstartup systemd -u $(whoami)'
169
- : 'pm2 unstartup';
201
+ const command = getUnstartupCommand();
170
202
 
171
203
  console.log(`Running unstartup command: ${command}`);
172
204
 
@@ -277,5 +309,8 @@ function createPm2AutostartRouter() {
277
309
 
278
310
  module.exports = createPm2AutostartRouter;
279
311
  module.exports._test = {
280
- getExecOptions
312
+ getExecOptions,
313
+ getPm2CliCommand,
314
+ getStartupCommand,
315
+ getUnstartupCommand
281
316
  };
@@ -19,8 +19,15 @@ const eventBus = require('../plugins/event-bus');
19
19
  const { getEffectiveApiKey } = require('./services/channels');
20
20
  const { persistProxyRequestSnapshot, persistClaudeRequestTemplate } = require('./services/request-logger');
21
21
  const { publishUsageLog, publishFailureLog } = require('./services/proxy-log-helper');
22
- const { redirectModel } = require('./services/base/proxy-utils');
23
- const { parseSSEUsage, mergeUsageIntoTokenData, createTokenData } = require('./services/base/response-usage-parser');
22
+ const { redirectModel, normalizeGatewaySourceType } = require('./services/base/proxy-utils');
23
+ const { parseSSEUsage, parseNonStreamingUsage, mergeUsageIntoTokenData, createTokenData } = require('./services/base/response-usage-parser');
24
+ const {
25
+ createClaudeStreamRecoveryState,
26
+ mergeClaudeStreamEvent,
27
+ buildAssistantMessageFromStreamState,
28
+ recoverClaudeUsageViaCountTokens
29
+ } = require('./services/claude-token-recovery');
30
+ const { handleClaudeOpenAiGatewayRequest } = require('./services/claude-openai-gateway');
24
31
  const { attachServerShutdownHandling, expediteServerShutdown } = require('./services/server-shutdown');
25
32
 
26
33
  let proxyServer = null;
@@ -172,6 +179,16 @@ function buildClaudeRequestSummary(req, sessionId = null) {
172
179
  };
173
180
  }
174
181
 
182
+ function clearTokenDataUsage(tokenData) {
183
+ tokenData.inputTokens = 0;
184
+ tokenData.outputTokens = 0;
185
+ tokenData.cacheCreation = 0;
186
+ tokenData.cacheRead = 0;
187
+ tokenData.cachedTokens = 0;
188
+ tokenData.reasoningTokens = 0;
189
+ tokenData.totalTokens = 0;
190
+ }
191
+
175
192
  async function startProxyServer(options = {}) {
176
193
  const preserveStartTime = options.preserveStartTime || false;
177
194
 
@@ -314,6 +331,21 @@ async function startProxyServer(options = {}) {
314
331
  }
315
332
  }
316
333
 
334
+ const gatewaySourceType = normalizeGatewaySourceType(channel.gatewaySourceType, 'claude');
335
+ if (gatewaySourceType === 'openai_compatible') {
336
+ const handled = await handleClaudeOpenAiGatewayRequest({
337
+ req,
338
+ res,
339
+ channel,
340
+ effectiveKey,
341
+ calculateCost,
342
+ onDone: release
343
+ });
344
+ if (handled) {
345
+ return;
346
+ }
347
+ }
348
+
317
349
  const proxyOptions = {
318
350
  target: channel.baseUrl,
319
351
  changeOrigin: true,
@@ -396,9 +428,16 @@ async function startProxyServer(options = {}) {
396
428
  requestMetadata.delete(req);
397
429
  });
398
430
 
431
+ const isSseResponse = proxyRes.headers['content-type']?.includes('text/event-stream');
399
432
  let buffer = '';
400
433
  let tokenData = createTokenData();
401
434
  let usageRecorded = false;
435
+ const streamRecoveryState = createClaudeStreamRecoveryState();
436
+ const usageObservation = {
437
+ sawAnyUsage: false,
438
+ sawMessageStartUsage: false,
439
+ sawNonInitialUsage: false
440
+ };
402
441
  const parsedStream = createDecodedStream(proxyRes);
403
442
 
404
443
  function recordUsageIfReady() {
@@ -434,6 +473,10 @@ async function startProxyServer(options = {}) {
434
473
 
435
474
  buffer += chunk.toString('utf8');
436
475
 
476
+ if (!isSseResponse) {
477
+ return;
478
+ }
479
+
437
480
  const events = buffer.split('\n\n');
438
481
  buffer = events.pop() || '';
439
482
 
@@ -456,10 +499,19 @@ async function startProxyServer(options = {}) {
456
499
  if (!data || data === '[DONE]') return;
457
500
 
458
501
  const parsed = JSON.parse(data);
502
+ mergeClaudeStreamEvent(streamRecoveryState, eventType, parsed);
459
503
  const usage = parseSSEUsage(parsed, eventType);
504
+ if (usage.tokens) {
505
+ usageObservation.sawAnyUsage = true;
506
+ if (eventType === 'message_start') {
507
+ usageObservation.sawMessageStartUsage = true;
508
+ } else {
509
+ usageObservation.sawNonInitialUsage = true;
510
+ }
511
+ }
460
512
  mergeUsageIntoTokenData(tokenData, usage);
461
513
 
462
- if (usage.isDone) {
514
+ if (usage.isDone && (!isSseResponse || usageObservation.sawNonInitialUsage)) {
463
515
  recordUsageIfReady();
464
516
  }
465
517
  } catch (err) {
@@ -477,8 +529,48 @@ async function startProxyServer(options = {}) {
477
529
  };
478
530
 
479
531
  parsedStream.on('end', () => {
480
- recordUsageIfReady();
481
- finalize();
532
+ void (async () => {
533
+ if (!isSseResponse) {
534
+ try {
535
+ const parsed = JSON.parse(buffer);
536
+ const usage = parseNonStreamingUsage(parsed);
537
+ mergeUsageIntoTokenData(tokenData, usage);
538
+ } catch (err) {
539
+ }
540
+ } else {
541
+ const assistantMessage = buildAssistantMessageFromStreamState(streamRecoveryState);
542
+ const shouldRecoverUsage = Boolean(assistantMessage)
543
+ && (!usageObservation.sawAnyUsage || (usageObservation.sawMessageStartUsage && !usageObservation.sawNonInitialUsage));
544
+
545
+ if (shouldRecoverUsage) {
546
+ try {
547
+ const recoveredUsage = await recoverClaudeUsageViaCountTokens({
548
+ baseUrl: req.selectedChannel?.baseUrl || '',
549
+ apiKey: req.effectiveApiKey || '',
550
+ requestBody: req.body,
551
+ assistantMessage
552
+ });
553
+ if (recoveredUsage) {
554
+ tokenData.model = tokenData.model || streamRecoveryState.model || '';
555
+ tokenData.inputTokens = Number(recoveredUsage.inputTokens || 0);
556
+ tokenData.outputTokens = Number(recoveredUsage.outputTokens || 0);
557
+ // Let publishUsageLog recompute Claude totals from the recovered
558
+ // prompt/output pair plus any cache fields already present.
559
+ tokenData.totalTokens = 0;
560
+ } else {
561
+ tokenData.model = tokenData.model || streamRecoveryState.model || '';
562
+ clearTokenDataUsage(tokenData);
563
+ }
564
+ } catch (error) {
565
+ tokenData.model = tokenData.model || streamRecoveryState.model || '';
566
+ clearTokenDataUsage(tokenData);
567
+ }
568
+ }
569
+ }
570
+
571
+ recordUsageIfReady();
572
+ finalize();
573
+ })();
482
574
  });
483
575
 
484
576
  parsedStream.on('error', (err) => {
@@ -5,6 +5,7 @@ const { isProxyConfig } = require('./settings-manager');
5
5
  const { PATHS, NATIVE_PATHS } = require('../../config/paths');
6
6
  const { clearNativeOAuth } = require('./native-oauth-adapters');
7
7
  const { isWindowsLikePlatform } = require('../../utils/home-dir');
8
+ const { normalizeGatewaySourceType } = require('./base/proxy-utils');
8
9
 
9
10
  // ── Claude 特有工具函数 ──
10
11
 
@@ -160,6 +161,27 @@ function resolveCurrentManagedChannel(channels = []) {
160
161
  return allChannels.find(ch => ch.enabled !== false) || null;
161
162
  }
162
163
 
164
+ function normalizeClaudeTargetApi(value) {
165
+ const normalized = String(value || '').trim().toLowerCase();
166
+ if (normalized === 'chat' || normalized === 'chat/completions' || normalized === 'chat.completions') {
167
+ return 'chat.completions';
168
+ }
169
+ return 'responses';
170
+ }
171
+
172
+ function isOfficialOpenAiBaseUrl(baseUrl = '') {
173
+ try {
174
+ const parsed = new URL(String(baseUrl || '').trim());
175
+ return String(parsed.hostname || '').trim().toLowerCase() === 'api.openai.com';
176
+ } catch {
177
+ return false;
178
+ }
179
+ }
180
+
181
+ function isOpenAiCompatibleGateway(channel) {
182
+ return normalizeGatewaySourceType(channel?.gatewaySourceType, 'claude') === 'openai_compatible';
183
+ }
184
+
163
185
  // ── ClaudeChannelService ──
164
186
 
165
187
  class ClaudeChannelService extends BaseChannelService {
@@ -179,6 +201,20 @@ class ClaudeChannelService extends BaseChannelService {
179
201
  return `channel-${Date.now()}`;
180
202
  }
181
203
 
204
+ _applyDefaults(channel) {
205
+ const normalized = super._applyDefaults(channel);
206
+ normalized.presetId = normalized.presetId || 'official';
207
+ normalized.modelConfig = normalized.modelConfig || {};
208
+ normalized.modelRedirects = Array.isArray(normalized.modelRedirects) ? normalized.modelRedirects : [];
209
+ normalized.proxyUrl = normalized.proxyUrl || '';
210
+ normalized.speedTestModel = normalized.speedTestModel || null;
211
+ normalized.targetApi = normalizeClaudeTargetApi(normalized.targetApi);
212
+ if (isOpenAiCompatibleGateway(normalized) && !isOfficialOpenAiBaseUrl(normalized.baseUrl)) {
213
+ normalized.targetApi = 'chat.completions';
214
+ }
215
+ return normalized;
216
+ }
217
+
182
218
  // Claude 使用缓存 + fs.watchFile
183
219
  loadChannels() {
184
220
  if (this._cacheInitialized && this._cache) {
@@ -209,7 +245,7 @@ class ClaudeChannelService extends BaseChannelService {
209
245
  }
210
246
 
211
247
  _onAfterCreate(channel, _allChannels) {
212
- if (!isProxyConfig() && channel.enabled !== false) {
248
+ if (!isProxyConfig() && channel.enabled !== false && !isOpenAiCompatibleGateway(channel)) {
213
249
  this._applyToNativeSettings(channel);
214
250
  }
215
251
  }
@@ -220,12 +256,18 @@ class ClaudeChannelService extends BaseChannelService {
220
256
  }
221
257
 
222
258
  if (oldChannel.enabled === false && nextChannel.enabled !== false) {
223
- this._applyToNativeSettings(nextChannel);
259
+ if (!isOpenAiCompatibleGateway(nextChannel)) {
260
+ this._applyToNativeSettings(nextChannel);
261
+ }
224
262
  return;
225
263
  }
226
264
 
227
265
  const activeChannel = resolveCurrentManagedChannel(allChannels);
228
- if (nextChannel.enabled !== false && activeChannel?.id === nextChannel.id) {
266
+ if (
267
+ nextChannel.enabled !== false
268
+ && activeChannel?.id === nextChannel.id
269
+ && !isOpenAiCompatibleGateway(nextChannel)
270
+ ) {
229
271
  this._applyToNativeSettings(nextChannel);
230
272
  }
231
273
  }
@@ -236,11 +278,27 @@ class ClaudeChannelService extends BaseChannelService {
236
278
  }
237
279
 
238
280
  const activeChannel = resolveCurrentManagedChannel(allChannels);
239
- if (activeChannel) {
281
+ if (activeChannel && !isOpenAiCompatibleGateway(activeChannel)) {
240
282
  this._applyToNativeSettings(activeChannel);
241
283
  }
242
284
  }
243
285
 
286
+ applyChannelToSettings(channelId) {
287
+ const data = this.loadChannels();
288
+ const channel = data.channels.find(ch => ch.id === channelId);
289
+ if (!channel) {
290
+ throw new Error('Channel not found');
291
+ }
292
+
293
+ if (isOpenAiCompatibleGateway(channel)) {
294
+ const error = new Error('OpenAI 格式渠道需要通过 Claude 代理使用,请先启动代理。');
295
+ error.statusCode = 400;
296
+ throw error;
297
+ }
298
+
299
+ return super.applyChannelToSettings(channelId);
300
+ }
301
+
244
302
  _applyToNativeSettings(channel) {
245
303
  updateClaudeSettingsWithModelConfig(channel);
246
304
  }