wtt-connect 0.2.62 → 0.2.63
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/package.json +1 -1
- package/src/runner.js +36 -6
- package/src/usage.js +207 -0
- package/src/wtt-api.js +13 -0
package/package.json
CHANGED
package/src/runner.js
CHANGED
|
@@ -21,6 +21,7 @@ import { runShellCommand } from './shell-runner.js';
|
|
|
21
21
|
import { TerminalSessionManager } from './terminal-session.js';
|
|
22
22
|
import { isModelSwitcherEnabled, switchModelProvider } from './model-switcher.js';
|
|
23
23
|
import { formatSlashCommandHelp, isAgentSlashCommand, parseSlashCommand, slashCommandsForAdapter } from './slash.js';
|
|
24
|
+
import { createUsageCollector, taskUsagePatch } from './usage.js';
|
|
24
25
|
|
|
25
26
|
const TERMINAL_STATUSES = new Set(['review', 'done', 'approved', 'cancelled']);
|
|
26
27
|
const GENERATED_FILE_EXTENSIONS = new Set([
|
|
@@ -184,12 +185,14 @@ export class Runner {
|
|
|
184
185
|
await this.wtt.typing(topicId, 'start', { statusText: 'Agent 已接收消息,正在准备执行', statusKind: 'queued', ttlMs: 30000 });
|
|
185
186
|
let runtimeSelection = null;
|
|
186
187
|
let chatStream = null;
|
|
188
|
+
let usageCollector = null;
|
|
187
189
|
try {
|
|
188
190
|
const transcripts = await this.transcribeAttachments(staged.files);
|
|
189
191
|
const modelConfig = modelConfigFromMessage(m);
|
|
190
192
|
const adapter = this.registry.select({ ...m, content, model_config: modelConfig });
|
|
193
|
+
usageCollector = createUsageCollector(adapter.name, modelConfig);
|
|
191
194
|
runtimeSelection = { adapter: adapter.name, modelConfig };
|
|
192
|
-
chatStream = this.createChatStream(topicId, adapter.name, modelConfig);
|
|
195
|
+
chatStream = this.createChatStream(topicId, adapter.name, modelConfig, () => usageCollector?.summary());
|
|
193
196
|
await chatStream.start();
|
|
194
197
|
this.recordRuntimeSelection(adapter.name, modelConfig, 'running');
|
|
195
198
|
await this.wtt.typing(topicId, 'start', { statusText: `${adapterDisplayName(adapter.name)} 正在执行${modelConfig.model ? ` · ${modelConfig.model}` : ''}`, statusKind: 'running', adapter: adapter.name, model: modelConfig.model || undefined, ttlMs: 30000 });
|
|
@@ -211,12 +214,16 @@ export class Runner {
|
|
|
211
214
|
modelConfig,
|
|
212
215
|
files: staged.files,
|
|
213
216
|
images: staged.images,
|
|
214
|
-
onProgress: (event) =>
|
|
217
|
+
onProgress: (event) => {
|
|
218
|
+
usageCollector?.observe(event);
|
|
219
|
+
return this.handleChatProgress(topicId, event, adapter.name, chatStream);
|
|
220
|
+
},
|
|
215
221
|
});
|
|
216
222
|
if (localSlash !== null) {
|
|
217
223
|
const reply = stripHiddenContextLeak(localSlash || '(empty response)') || '(empty response)';
|
|
218
224
|
await chatStream.finish(reply);
|
|
219
225
|
await this.wtt.publish(topicId, reply, 'CHAT_REPLY', chatStream.finalMetadata());
|
|
226
|
+
await this.reportUsageBestEffort(usageCollector?.summary(), { runId: chatStream.id, topicId, messageId: m.id });
|
|
220
227
|
log('info', 'slash command replied', { topicId, adapter: adapter.name, chars: reply.length });
|
|
221
228
|
return;
|
|
222
229
|
}
|
|
@@ -255,13 +262,17 @@ export class Runner {
|
|
|
255
262
|
modelConfig,
|
|
256
263
|
files: staged.files,
|
|
257
264
|
images: staged.images,
|
|
258
|
-
onProgress: (event) =>
|
|
265
|
+
onProgress: (event) => {
|
|
266
|
+
usageCollector?.observe(event);
|
|
267
|
+
return this.handleChatProgress(topicId, event, adapter.name, chatStream);
|
|
268
|
+
},
|
|
259
269
|
});
|
|
260
270
|
const prepared = prepareGeneratedFileArtifacts(stripHiddenContextLeak(output || '(empty response)') || '(empty response)');
|
|
261
271
|
const reply = prepared.text || '(empty response)';
|
|
262
272
|
await chatStream.finish(reply);
|
|
263
273
|
await this.maybeAttachSpeech(topicId, reply, `chat-${topicId}`);
|
|
264
274
|
await this.wtt.publish(topicId, reply, 'CHAT_REPLY', chatStream.finalMetadata());
|
|
275
|
+
await this.reportUsageBestEffort(usageCollector?.summary(), { runId: chatStream.id, topicId, messageId: m.id });
|
|
265
276
|
await this.publishGeneratedFileArtifacts(topicId, prepared.refs, { source: 'chat', sourceId: topicId, adapter: adapter.name });
|
|
266
277
|
log('info', 'chat replied', { topicId, chars: reply.length });
|
|
267
278
|
} catch (err) {
|
|
@@ -284,6 +295,7 @@ export class Runner {
|
|
|
284
295
|
const transcripts = await this.transcribeAttachments(staged.files);
|
|
285
296
|
const modelConfig = modelConfigFromMessage(task);
|
|
286
297
|
const adapter = this.registry.select({ ...task, model_config: modelConfig });
|
|
298
|
+
const usageCollector = createUsageCollector(adapter.name, modelConfig);
|
|
287
299
|
this.recordRuntimeSelection(adapter.name, modelConfig, 'running');
|
|
288
300
|
const agentProfile = await this.getAgentProfile();
|
|
289
301
|
if (topicId) await this.wtt.typing(topicId, 'start', { statusText: `${adapterDisplayName(adapter.name)} 正在执行任务${modelConfig.model ? ` · ${modelConfig.model}` : ''}`, statusKind: 'running', adapter: adapter.name, model: modelConfig.model || undefined, ttlMs: 30000 });
|
|
@@ -314,14 +326,18 @@ export class Runner {
|
|
|
314
326
|
modelConfig,
|
|
315
327
|
files: staged.files,
|
|
316
328
|
images: staged.images,
|
|
317
|
-
onProgress: (ev) =>
|
|
329
|
+
onProgress: (ev) => {
|
|
330
|
+
usageCollector.observe(ev);
|
|
331
|
+
return topicId ? this.maybePublishProgress(topicId, ev, adapter.name) : Promise.resolve();
|
|
332
|
+
},
|
|
318
333
|
});
|
|
319
334
|
const prepared = prepareGeneratedFileArtifacts(output || '(empty response)');
|
|
320
335
|
const summary = prepared.text || '(empty response)';
|
|
321
336
|
await this.maybeRelayArenaOpenCLResult(task, summary);
|
|
322
337
|
const artifact = await this.materializeTaskArtifact(taskId, summary);
|
|
323
338
|
const commitIds = extractCommitIds(summary);
|
|
324
|
-
const
|
|
339
|
+
const usage = usageCollector.summary();
|
|
340
|
+
const patch = { status: 'review', progress: 100, output: summary, summary, usage_source: `wtt-connect/${adapter.name}`, ...taskUsagePatch(usage) };
|
|
325
341
|
if (commitIds.length) patch.commit_id = commitIds[0];
|
|
326
342
|
await this.api.patchTask(taskId, patch);
|
|
327
343
|
if (topicId) {
|
|
@@ -329,6 +345,7 @@ export class Runner {
|
|
|
329
345
|
await this.publishGeneratedFileArtifacts(topicId, prepared.refs, { source: 'task', sourceId: taskId, adapter: adapter.name });
|
|
330
346
|
if (artifact?.asset?.url) await this.wtt.publish(topicId, `Artifact: ${artifact.asset.url}`, 'TASK_ARTIFACT');
|
|
331
347
|
}
|
|
348
|
+
await this.reportUsageBestEffort(usage, { runId: `task-${taskId}-${Date.now().toString(36)}`, topicId, taskId });
|
|
332
349
|
log('info', 'task completed', { taskId, chars: summary.length });
|
|
333
350
|
} catch (err) {
|
|
334
351
|
await this.api.patchTask(taskId, { status: 'blocked', output: String(err.message || err) });
|
|
@@ -626,7 +643,7 @@ export class Runner {
|
|
|
626
643
|
}
|
|
627
644
|
}
|
|
628
645
|
|
|
629
|
-
createChatStream(topicId, adapterName = 'agent', modelConfig = {}) {
|
|
646
|
+
createChatStream(topicId, adapterName = 'agent', modelConfig = {}, usageProvider = null) {
|
|
630
647
|
const enabled = this.config.messageStream !== false && Boolean(topicId);
|
|
631
648
|
const streamId = `wtt-${Date.now().toString(36)}-${crypto.randomUUID().slice(0, 8)}`;
|
|
632
649
|
let seq = 0;
|
|
@@ -649,11 +666,13 @@ export class Runner {
|
|
|
649
666
|
return {
|
|
650
667
|
id: streamId,
|
|
651
668
|
finalMetadata() {
|
|
669
|
+
const usage = typeof usageProvider === 'function' ? usageProvider() : null;
|
|
652
670
|
return {
|
|
653
671
|
stream_id: streamId,
|
|
654
672
|
streamId,
|
|
655
673
|
adapter: adapterName,
|
|
656
674
|
model: modelConfig.model || modelConfig.model_id || modelConfig.modelId || '',
|
|
675
|
+
...(usage ? { usage } : {}),
|
|
657
676
|
};
|
|
658
677
|
},
|
|
659
678
|
async start() {
|
|
@@ -699,6 +718,17 @@ export class Runner {
|
|
|
699
718
|
]);
|
|
700
719
|
}
|
|
701
720
|
|
|
721
|
+
async reportUsageBestEffort(usage, { runId = '', topicId = '', taskId = '', messageId = '' } = {}) {
|
|
722
|
+
if (!usage) return;
|
|
723
|
+
await this.api.reportAgentUsage({
|
|
724
|
+
...usage,
|
|
725
|
+
run_id: runId,
|
|
726
|
+
topic_id: topicId,
|
|
727
|
+
task_id: taskId,
|
|
728
|
+
message_id: messageId,
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
|
|
702
732
|
async maybePublishProgress(topicId, event, adapterName = 'agent') {
|
|
703
733
|
if (!topicId) return;
|
|
704
734
|
const normalized = normalizeAgentEvent(adapterName, event);
|
package/src/usage.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
export function createUsageCollector(adapter = 'agent', modelConfig = {}) {
|
|
2
|
+
let best = null;
|
|
3
|
+
let responseModel = '';
|
|
4
|
+
let rawUsage = null;
|
|
5
|
+
|
|
6
|
+
function observe(event) {
|
|
7
|
+
if (!event || typeof event !== 'object') return;
|
|
8
|
+
responseModel = extractModel(event) || responseModel;
|
|
9
|
+
const usage = extractUsage(event);
|
|
10
|
+
if (!usage) return;
|
|
11
|
+
rawUsage = usage.raw_usage || usage;
|
|
12
|
+
if (!best || scoreUsage(usage) >= scoreUsage(best)) best = usage;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function summary() {
|
|
16
|
+
if (!best) return null;
|
|
17
|
+
const inputTokens = safeInt(best.input_tokens);
|
|
18
|
+
const outputTokens = safeInt(best.output_tokens);
|
|
19
|
+
const cacheReadTokens = safeInt(best.cache_read_tokens);
|
|
20
|
+
const cacheWriteTokens = safeInt(best.cache_write_tokens);
|
|
21
|
+
const cacheTokens = safeInt(best.cache_tokens) + cacheReadTokens + cacheWriteTokens;
|
|
22
|
+
const totalTokens = safeInt(best.total_tokens) || inputTokens + outputTokens + cacheTokens;
|
|
23
|
+
if (!totalTokens && !inputTokens && !outputTokens && !cacheTokens) return null;
|
|
24
|
+
return {
|
|
25
|
+
adapter,
|
|
26
|
+
provider: providerForAdapter(adapter),
|
|
27
|
+
request_model: modelFromConfig(modelConfig),
|
|
28
|
+
response_model: responseModel,
|
|
29
|
+
input_tokens: inputTokens,
|
|
30
|
+
output_tokens: outputTokens,
|
|
31
|
+
cache_read_tokens: cacheReadTokens,
|
|
32
|
+
cache_write_tokens: cacheWriteTokens,
|
|
33
|
+
cache_tokens: cacheTokens,
|
|
34
|
+
total_tokens: totalTokens,
|
|
35
|
+
source: sourceForAdapter(adapter),
|
|
36
|
+
raw_usage: compactRawUsage(rawUsage),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { observe, summary };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function taskUsagePatch(usage) {
|
|
44
|
+
if (!usage) return {};
|
|
45
|
+
return {
|
|
46
|
+
usage_prompt_tokens: safeInt(usage.input_tokens),
|
|
47
|
+
usage_completion_tokens: safeInt(usage.output_tokens),
|
|
48
|
+
usage_cache_read_tokens: safeInt(usage.cache_read_tokens) + safeInt(usage.cache_tokens),
|
|
49
|
+
usage_cache_write_tokens: safeInt(usage.cache_write_tokens),
|
|
50
|
+
usage_total_tokens: safeInt(usage.total_tokens),
|
|
51
|
+
usage_source: usage.source || 'wtt-connect',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function extractUsage(event) {
|
|
56
|
+
const direct = findUsageObject(event);
|
|
57
|
+
if (direct) return normalizeUsage(direct);
|
|
58
|
+
const stats = event?.stats;
|
|
59
|
+
if (stats && typeof stats === 'object') {
|
|
60
|
+
const models = stats.models;
|
|
61
|
+
if (models && typeof models === 'object' && !Array.isArray(models)) {
|
|
62
|
+
return normalizeUsage(aggregateModelStats(models));
|
|
63
|
+
}
|
|
64
|
+
return normalizeUsage(stats);
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function findUsageObject(value, depth = 0) {
|
|
70
|
+
if (!value || typeof value !== 'object' || depth > 5) return null;
|
|
71
|
+
for (const key of ['usage', 'usage_metadata', 'usageMetadata', 'token_usage', 'tokenUsage']) {
|
|
72
|
+
const candidate = value[key];
|
|
73
|
+
if (candidate && typeof candidate === 'object') return candidate;
|
|
74
|
+
}
|
|
75
|
+
if (looksLikeUsage(value)) return value;
|
|
76
|
+
for (const key of ['message', 'response', 'result', 'item', 'delta', 'event']) {
|
|
77
|
+
const found = findUsageObject(value[key], depth + 1);
|
|
78
|
+
if (found) return found;
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function looksLikeUsage(value) {
|
|
84
|
+
if (!value || typeof value !== 'object') return false;
|
|
85
|
+
return [
|
|
86
|
+
'input_tokens',
|
|
87
|
+
'prompt_tokens',
|
|
88
|
+
'output_tokens',
|
|
89
|
+
'completion_tokens',
|
|
90
|
+
'total_tokens',
|
|
91
|
+
'promptTokenCount',
|
|
92
|
+
'candidatesTokenCount',
|
|
93
|
+
'totalTokenCount',
|
|
94
|
+
].some((key) => key in value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizeUsage(raw) {
|
|
98
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
99
|
+
const inputTokens = firstInt(raw, [
|
|
100
|
+
'input_tokens',
|
|
101
|
+
'prompt_tokens',
|
|
102
|
+
'promptTokens',
|
|
103
|
+
'prompt_token_count',
|
|
104
|
+
'promptTokenCount',
|
|
105
|
+
'inputTokenCount',
|
|
106
|
+
]);
|
|
107
|
+
const candidateTokens = firstInt(raw, [
|
|
108
|
+
'output_tokens',
|
|
109
|
+
'completion_tokens',
|
|
110
|
+
'completionTokens',
|
|
111
|
+
'completion_token_count',
|
|
112
|
+
'candidatesTokenCount',
|
|
113
|
+
'outputTokenCount',
|
|
114
|
+
]);
|
|
115
|
+
const thoughtsTokens = firstInt(raw, ['thoughtsTokenCount', 'reasoning_tokens', 'reasoningTokens']);
|
|
116
|
+
const outputTokens = candidateTokens + thoughtsTokens;
|
|
117
|
+
const cacheReadTokens = firstInt(raw, [
|
|
118
|
+
'cache_read_input_tokens',
|
|
119
|
+
'cached_tokens',
|
|
120
|
+
'cachedTokens',
|
|
121
|
+
'cachedContentTokenCount',
|
|
122
|
+
]);
|
|
123
|
+
const cacheWriteTokens = firstInt(raw, ['cache_creation_input_tokens', 'cache_write_input_tokens']);
|
|
124
|
+
const cacheTokens = firstInt(raw, ['cache_tokens', 'cacheTokens']);
|
|
125
|
+
const totalTokens = firstInt(raw, ['total_tokens', 'totalTokens', 'totalTokenCount'])
|
|
126
|
+
|| inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens + cacheTokens;
|
|
127
|
+
if (!inputTokens && !outputTokens && !cacheReadTokens && !cacheWriteTokens && !cacheTokens && !totalTokens) return null;
|
|
128
|
+
return {
|
|
129
|
+
input_tokens: inputTokens,
|
|
130
|
+
output_tokens: outputTokens,
|
|
131
|
+
cache_read_tokens: cacheReadTokens,
|
|
132
|
+
cache_write_tokens: cacheWriteTokens,
|
|
133
|
+
cache_tokens: cacheTokens,
|
|
134
|
+
total_tokens: totalTokens,
|
|
135
|
+
raw_usage: raw,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function aggregateModelStats(models) {
|
|
140
|
+
const totals = {};
|
|
141
|
+
for (const value of Object.values(models || {})) {
|
|
142
|
+
if (!value || typeof value !== 'object') continue;
|
|
143
|
+
const usage = normalizeUsage(value);
|
|
144
|
+
if (!usage) continue;
|
|
145
|
+
for (const key of ['input_tokens', 'output_tokens', 'cache_read_tokens', 'cache_write_tokens', 'cache_tokens', 'total_tokens']) {
|
|
146
|
+
totals[key] = safeInt(totals[key]) + safeInt(usage[key]);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return totals;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function extractModel(event) {
|
|
153
|
+
if (!event || typeof event !== 'object') return '';
|
|
154
|
+
for (const key of ['model', 'model_id', 'modelId', 'current_model', 'response_model']) {
|
|
155
|
+
const value = event[key];
|
|
156
|
+
if (typeof value === 'string' && value.trim()) return value.trim();
|
|
157
|
+
}
|
|
158
|
+
return extractModel(event.message) || extractModel(event.response) || '';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function modelFromConfig(modelConfig = {}) {
|
|
162
|
+
return String(modelConfig.model || modelConfig.model_id || modelConfig.modelId || '').trim();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function firstInt(raw, keys) {
|
|
166
|
+
for (const key of keys) {
|
|
167
|
+
const value = raw?.[key];
|
|
168
|
+
const num = safeInt(value);
|
|
169
|
+
if (num) return num;
|
|
170
|
+
}
|
|
171
|
+
return 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function safeInt(value) {
|
|
175
|
+
const num = Number(value || 0);
|
|
176
|
+
if (!Number.isFinite(num) || num <= 0) return 0;
|
|
177
|
+
return Math.round(num);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function scoreUsage(usage) {
|
|
181
|
+
return safeInt(usage?.total_tokens) || safeInt(usage?.input_tokens) + safeInt(usage?.output_tokens) + safeInt(usage?.cache_tokens);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function providerForAdapter(adapter) {
|
|
185
|
+
if (adapter === 'claude-code') return 'anthropic-compatible';
|
|
186
|
+
if (adapter === 'codex') return 'openai-compatible';
|
|
187
|
+
if (adapter === 'gemini') return 'gemini';
|
|
188
|
+
return adapter || 'agent';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function sourceForAdapter(adapter) {
|
|
192
|
+
if (adapter === 'claude-code') return 'claude_code_stream_json';
|
|
193
|
+
if (adapter === 'codex') return 'codex_json';
|
|
194
|
+
if (adapter === 'gemini') return 'gemini_stream_json';
|
|
195
|
+
return `wtt-connect/${adapter || 'agent'}`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function compactRawUsage(value) {
|
|
199
|
+
if (!value || typeof value !== 'object') return undefined;
|
|
200
|
+
try {
|
|
201
|
+
const json = JSON.stringify(value);
|
|
202
|
+
if (json.length <= 4000) return value;
|
|
203
|
+
return { truncated: true, total_tokens: safeInt(value.total_tokens || value.totalTokens || value.totalTokenCount) };
|
|
204
|
+
} catch {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
}
|
package/src/wtt-api.js
CHANGED
|
@@ -117,4 +117,17 @@ export class WTTApi {
|
|
|
117
117
|
});
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
async reportAgentUsage(usage) {
|
|
121
|
+
if (!usage || !this.config.agentId || !this.config.token) return null;
|
|
122
|
+
try {
|
|
123
|
+
return await this.request('POST', `/agents/${encodeURIComponent(this.config.agentId)}/usage`, {
|
|
124
|
+
headers: this.agentHeaders(),
|
|
125
|
+
json: usage,
|
|
126
|
+
});
|
|
127
|
+
} catch (err) {
|
|
128
|
+
log('warn', 'report_agent_usage failed', { error: err.message });
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
120
133
|
}
|