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.
- package/CHANGELOG.md +15 -0
- package/dist/web/assets/{Analytics-B9Shk-uq.js → Analytics-DvWZOZ13.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-Dlkn-_zW.js → ConfigTemplates-DiuKu3LA.js} +1 -1
- package/dist/web/assets/{Home-AGf32LvJ.js → Home-DeGTc9uf.js} +1 -1
- package/dist/web/assets/{PluginManager-BnAjHN4u.js → PluginManager-CljkN0Rb.js} +1 -1
- package/dist/web/assets/{ProjectList-7YV_zk9L.js → ProjectList-CFQJaSQ8.js} +1 -1
- package/dist/web/assets/{SessionList-D0un-UKS.js → SessionList-Fq4uQSQp.js} +1 -1
- package/dist/web/assets/{SkillManager-DhYBmbtd.js → SkillManager-DLhg11LF.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-JOZIPAGo.js → WorkspaceManager-DXB4kE9p.js} +1 -1
- package/dist/web/assets/index-CLra7uzu.js +2 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/src/commands/daemon.js +306 -98
- package/src/config/channel-preset-websites.js +1 -0
- package/src/index.js +18 -11
- package/src/server/api/channels.js +62 -17
- package/src/server/api/codex-channels.js +12 -1
- package/src/server/api/pm2-autostart.js +59 -24
- package/src/server/proxy-server.js +97 -5
- package/src/server/services/channels.js +62 -4
- package/src/server/services/claude-openai-gateway.js +1259 -0
- package/src/server/services/claude-token-recovery.js +386 -0
- package/src/server/services/codex-channels.js +8 -7
- package/src/server/services/proxy-log-helper.js +9 -4
- package/dist/web/assets/index-DM3yF1Oo.js +0 -2
|
@@ -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 =
|
|
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 =
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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 =
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
481
|
-
|
|
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
|
-
|
|
259
|
+
if (!isOpenAiCompatibleGateway(nextChannel)) {
|
|
260
|
+
this._applyToNativeSettings(nextChannel);
|
|
261
|
+
}
|
|
224
262
|
return;
|
|
225
263
|
}
|
|
226
264
|
|
|
227
265
|
const activeChannel = resolveCurrentManagedChannel(allChannels);
|
|
228
|
-
if (
|
|
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
|
}
|