coding-tool-x 3.5.12 → 3.5.14
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-GuTri2mr.js → Analytics-D24Q2IkD.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-DILbDhRj.js → ConfigTemplates-xNm_dyGS.js} +1 -1
- package/dist/web/assets/Home-BeGkC4bb.css +1 -0
- package/dist/web/assets/{Home-ri7cXeJp.js → Home-YQrz9dNs.js} +1 -1
- package/dist/web/assets/{PluginManager-DQPeUFkr.js → PluginManager-7ULrS-17.js} +1 -1
- package/dist/web/assets/{ProjectList-Cpf82Lgb.js → ProjectList-Fy4xyQpg.js} +1 -1
- package/dist/web/assets/{SessionList-DXhPIwJI.js → SessionList-DcLW9x3Z.js} +1 -1
- package/dist/web/assets/{SkillManager-DNVyDlEv.js → SkillManager-RnoRbzHA.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-Cp0t7cjn.js → WorkspaceManager-B_BWS-xG.js} +1 -1
- package/dist/web/assets/index-Bvdezm5-.js +2 -0
- package/dist/web/assets/{index-DfikIh5S.css → index-dzKgnRhx.css} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/commands/daemon.js +256 -80
- package/src/config/channel-preset-websites.js +1 -0
- package/src/server/api/channels.js +62 -17
- package/src/server/api/codex-channels.js +23 -3
- package/src/server/api/pm2-autostart.js +59 -24
- package/src/server/opencode-proxy-server.js +14 -8
- package/src/server/proxy-server.js +97 -5
- package/src/server/services/base/response-usage-parser.js +40 -13
- 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/dist/web/assets/Home-Bm7mK2qz.css +0 -1
- package/dist/web/assets/index-C5wcoiDy.js +0 -2
|
@@ -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
|
};
|
|
@@ -1924,6 +1924,12 @@ function reportOpenCodeGatewayFailure({
|
|
|
1924
1924
|
}
|
|
1925
1925
|
|
|
1926
1926
|
function publishOpenCodeUsageLog({ requestId, channel, model, usage, startTime }) {
|
|
1927
|
+
const parsedUsage = parseNonStreamingUsage({
|
|
1928
|
+
model: model || '',
|
|
1929
|
+
usage: usage && typeof usage === 'object' ? usage : {}
|
|
1930
|
+
});
|
|
1931
|
+
const parsedTokens = parsedUsage.tokens || {};
|
|
1932
|
+
|
|
1927
1933
|
return publishUsageLog({
|
|
1928
1934
|
source: 'opencode',
|
|
1929
1935
|
metadata: {
|
|
@@ -1932,15 +1938,15 @@ function publishOpenCodeUsageLog({ requestId, channel, model, usage, startTime }
|
|
|
1932
1938
|
channelId: channel?.id,
|
|
1933
1939
|
startTime
|
|
1934
1940
|
},
|
|
1935
|
-
model: model || '',
|
|
1941
|
+
model: parsedUsage.model || model || '',
|
|
1936
1942
|
tokens: {
|
|
1937
|
-
input: Number(
|
|
1938
|
-
output: Number(
|
|
1939
|
-
cacheCreation: Number(
|
|
1940
|
-
cacheRead: Number(
|
|
1941
|
-
cached: Number(
|
|
1942
|
-
reasoning: Number(
|
|
1943
|
-
total: Number(
|
|
1943
|
+
input: Number(parsedTokens.input || 0),
|
|
1944
|
+
output: Number(parsedTokens.output || 0),
|
|
1945
|
+
cacheCreation: Number(parsedTokens.cacheCreation || 0),
|
|
1946
|
+
cacheRead: Number(parsedTokens.cacheRead || 0),
|
|
1947
|
+
cached: Number(parsedTokens.cached || 0),
|
|
1948
|
+
reasoning: Number(parsedTokens.reasoning || 0),
|
|
1949
|
+
total: Number(parsedTokens.total || 0)
|
|
1944
1950
|
},
|
|
1945
1951
|
calculateCost,
|
|
1946
1952
|
broadcastLog,
|
|
@@ -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) => {
|
|
@@ -64,6 +64,15 @@ function parseUsageObject(rawUsage) {
|
|
|
64
64
|
rawUsage.inputTokensDetails !== undefined ||
|
|
65
65
|
rawUsage.output_tokens_details !== undefined ||
|
|
66
66
|
rawUsage.outputTokensDetails !== undefined;
|
|
67
|
+
const hasOpenAiCompatibilityFields =
|
|
68
|
+
hasOpenAiPromptFields ||
|
|
69
|
+
hasOpenAiResponseFields ||
|
|
70
|
+
rawUsage.cached_tokens !== undefined ||
|
|
71
|
+
rawUsage.cachedTokens !== undefined ||
|
|
72
|
+
rawUsage.reasoning_tokens !== undefined ||
|
|
73
|
+
rawUsage.reasoningTokens !== undefined ||
|
|
74
|
+
rawUsage.completion_tokens_details !== undefined ||
|
|
75
|
+
rawUsage.completionTokensDetails !== undefined;
|
|
67
76
|
|
|
68
77
|
const input = readNumericField(rawUsage, ['input_tokens', 'prompt_tokens', 'inputTokens', 'promptTokens']);
|
|
69
78
|
if (input !== undefined) tokens.input = input;
|
|
@@ -74,31 +83,43 @@ function parseUsageObject(rawUsage) {
|
|
|
74
83
|
const total = readNumericField(rawUsage, ['total_tokens', 'totalTokens']);
|
|
75
84
|
if (total !== undefined) tokens.total = total;
|
|
76
85
|
|
|
77
|
-
const cacheCreation = readNumericField(rawUsage, ['cache_creation_input_tokens', 'cacheCreationInputTokens'])
|
|
86
|
+
const cacheCreation = readNumericField(rawUsage, ['cache_creation_input_tokens', 'cacheCreationInputTokens'])
|
|
87
|
+
?? readNestedNumericField(rawUsage, [
|
|
88
|
+
['prompt_tokens_details', 'cache_creation_input_tokens'],
|
|
89
|
+
['promptTokensDetails', 'cacheCreationInputTokens']
|
|
90
|
+
]);
|
|
78
91
|
if (cacheCreation !== undefined) tokens.cacheCreation = cacheCreation;
|
|
79
92
|
|
|
80
93
|
const cacheRead = readNumericField(rawUsage, ['cache_read_input_tokens', 'cacheReadInputTokens']);
|
|
81
94
|
if (cacheRead !== undefined) tokens.cacheRead = cacheRead;
|
|
82
95
|
|
|
83
|
-
const cached =
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
96
|
+
const cached = readNumericField(rawUsage, ['cached_tokens', 'cachedTokens'])
|
|
97
|
+
?? readNestedNumericField(rawUsage, [
|
|
98
|
+
['input_tokens_details', 'cached_tokens'],
|
|
99
|
+
['prompt_tokens_details', 'cached_tokens'],
|
|
100
|
+
['inputTokensDetails', 'cachedTokens'],
|
|
101
|
+
['promptTokensDetails', 'cachedTokens']
|
|
102
|
+
]);
|
|
89
103
|
if (cached !== undefined) tokens.cached = cached;
|
|
90
104
|
|
|
91
|
-
const reasoning =
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
105
|
+
const reasoning = readNumericField(rawUsage, ['reasoning_tokens', 'reasoningTokens'])
|
|
106
|
+
?? readNestedNumericField(rawUsage, [
|
|
107
|
+
['completion_tokens_details', 'reasoning_tokens'],
|
|
108
|
+
['output_tokens_details', 'reasoning_tokens'],
|
|
109
|
+
['completionTokensDetails', 'reasoningTokens'],
|
|
110
|
+
['outputTokensDetails', 'reasoningTokens']
|
|
111
|
+
]);
|
|
95
112
|
if (reasoning !== undefined) tokens.reasoning = reasoning;
|
|
96
113
|
|
|
114
|
+
if (tokens.total === undefined && tokens.input !== undefined && tokens.output !== undefined && !hasAnthropicCacheFields) {
|
|
115
|
+
tokens.total = tokens.input + tokens.output;
|
|
116
|
+
}
|
|
117
|
+
|
|
97
118
|
// OpenAI/OpenAI-compatible usage commonly reports prompt/input totals that
|
|
98
119
|
// already include cache hits. Align with OpenCode's provider contract by
|
|
99
120
|
// moving cached prompt tokens into a dedicated field and keeping input as the
|
|
100
121
|
// net uncached prompt tokens.
|
|
101
|
-
if (!hasAnthropicCacheFields &&
|
|
122
|
+
if (!hasAnthropicCacheFields && hasOpenAiCompatibilityFields && tokens.cached !== undefined && tokens.input !== undefined) {
|
|
102
123
|
tokens.input = Math.max(tokens.input - tokens.cached, 0);
|
|
103
124
|
}
|
|
104
125
|
|
|
@@ -132,13 +153,16 @@ function parseSSEUsage(parsed, eventType) {
|
|
|
132
153
|
if (eventType === 'message_start' && parsed.message && parsed.message.model) {
|
|
133
154
|
model = parsed.message.model;
|
|
134
155
|
}
|
|
156
|
+
if (!tokens && parsed.message && parsed.message.usage) {
|
|
157
|
+
tokens = parseUsageObject(parsed.message.usage);
|
|
158
|
+
}
|
|
135
159
|
if (eventType === 'message_stop') {
|
|
136
160
|
isDone = true;
|
|
137
161
|
}
|
|
138
162
|
|
|
139
163
|
// === OpenAI Responses API 格式 ===
|
|
140
164
|
// data: {"type": "response.completed", "response": {"model", "usage": {...}}}
|
|
141
|
-
if (parsed.type === 'response.completed' && parsed.response) {
|
|
165
|
+
if ((parsed.type === 'response.completed' || parsed.type === 'response.incomplete') && parsed.response) {
|
|
142
166
|
if (parsed.response.model) {
|
|
143
167
|
model = parsed.response.model;
|
|
144
168
|
}
|
|
@@ -171,6 +195,9 @@ function parseSSEUsage(parsed, eventType) {
|
|
|
171
195
|
if (parsed.usageMetadata.cachedContentTokenCount) {
|
|
172
196
|
tokens.cached = parsed.usageMetadata.cachedContentTokenCount;
|
|
173
197
|
}
|
|
198
|
+
if (parsed.usageMetadata.thoughtsTokenCount) {
|
|
199
|
+
tokens.reasoning = parsed.usageMetadata.thoughtsTokenCount;
|
|
200
|
+
}
|
|
174
201
|
}
|
|
175
202
|
|
|
176
203
|
// === 通用 model fallback ===
|
|
@@ -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
|
}
|