opencode2-tps 0.2.0-rc.1 → 0.2.1-rc.1
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/README.md +7 -9
- package/dist/tui.js +179 -165
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,13 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Live token-throughput indicator for the OpenCode 2 TUI prompt composer.
|
|
4
4
|
|
|
5
|
-
While a session streams, the top right of the composer shows
|
|
5
|
+
While a session streams, the top right of the composer shows estimated observable tokens and throughput: `~1712 tok · ~51.5 t/s`
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
1712 tok · 51.5 t/s
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
When the run ends, the number freezes at the run average and stays there until the next run starts. Nothing is shown on the home screen, or before the first token of a run.
|
|
7
|
+
When OpenCode reports terminal usage, the token count becomes exact while TPS remains approximate, for example `1715 tok · ~51.5 t/s`. The result freezes until the next run starts. Nothing is shown on the home screen, or before the first observable output.
|
|
12
8
|
|
|
13
9
|
<p align="center">
|
|
14
10
|
<img src="docs/screenshots/opencode2_tps.png" width="750" alt="Composer showing live token-throughput indicator" />
|
|
@@ -16,7 +12,7 @@ When the run ends, the number freezes at the run average and stays there until t
|
|
|
16
12
|
|
|
17
13
|
## Install
|
|
18
14
|
|
|
19
|
-
Built against the OpenCode 2 preview. The earliest known compatible beta is `0.0.0-beta-17595`; the latest tested beta is `0.0.0-beta-17639`. The TUI plugin API is still moving, so a much newer or older build may drop the indicator without an error
|
|
15
|
+
Built against the OpenCode 2 preview. The earliest known compatible beta is `0.0.0-beta-17595`; the latest tested beta is `0.0.0-beta-17639`. The TUI plugin API is still moving, so a much newer or older build may drop the indicator without an error. If the figure never appears, check your version first.
|
|
20
16
|
|
|
21
17
|
Add the package to `~/.config/opencode/cli.json`:
|
|
22
18
|
|
|
@@ -69,9 +65,11 @@ The defaults are usable as they are. For the full option list, the ranges and mo
|
|
|
69
65
|
|
|
70
66
|
## How it works
|
|
71
67
|
|
|
72
|
-
|
|
68
|
+
While output streams, the plugin estimates tokens from observable UTF-8 bytes at a default of 4.75 bytes per token and calculates a bounded rolling delivery rate. Complete text, reasoning, and tool-input events reconcile buffered or missed deltas without creating artificial live-rate spikes.
|
|
69
|
+
|
|
70
|
+
At the end of each model step, OpenCode's reported output and reasoning usage replaces the byte estimate. Settled TPS divides those exact tokens by observed step spans ending at the final model-content boundary, which excludes later local tool execution and time between model calls.
|
|
73
71
|
|
|
74
|
-
|
|
72
|
+
TPS is always approximate (`~`) because OpenCode does not expose token-level timestamps. Proprietary reasoning may be encrypted or represented only by a short summary, and some providers buffer tool arguments until completion. During those opaque intervals the live rate holds or becomes unavailable instead of continuously falling. Opaque provider state is never counted by byte length.
|
|
75
73
|
|
|
76
74
|
For more detail, see [Architecture](docs/development.md#architecture).
|
|
77
75
|
|
package/dist/tui.js
CHANGED
|
@@ -78,7 +78,8 @@ export function isEnvEnabled(value) {
|
|
|
78
78
|
function mark(line) {
|
|
79
79
|
if (!debugState.enabled) return;
|
|
80
80
|
try {
|
|
81
|
-
|
|
81
|
+
const safeLine = line.replace(/\p{Cc}/gu, character => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
82
|
+
appendFileSync(debugState.file, `${new Date().toISOString()} ${safeLine}\n`);
|
|
82
83
|
} catch {
|
|
83
84
|
// debug only; never break the host
|
|
84
85
|
}
|
|
@@ -92,15 +93,11 @@ export const DEFAULT_CONFIG = {
|
|
|
92
93
|
};
|
|
93
94
|
// The frozen final average stays visible until the next prompt starts a new run.
|
|
94
95
|
|
|
95
|
-
// Step completion events carry the generated-token count for that model call.
|
|
96
|
-
// Pairing it with deltas from the same assistant message avoids contamination
|
|
97
|
-
// from concurrent title generation and other session-level usage updates.
|
|
98
|
-
export const CALIBRATION_MIN_BYTES = 2_048;
|
|
99
|
-
export const CALIBRATION_MIN_TOKENS = 50;
|
|
100
|
-
// Bounds shared by the option clamp and the calibration sanity check: a ratio
|
|
101
|
-
// outside them means the report does not describe the bytes the plugin watched.
|
|
102
96
|
const BYTES_PER_TOKEN_MIN = 1;
|
|
103
97
|
const BYTES_PER_TOKEN_MAX = 16;
|
|
98
|
+
const LIVE_WINDOW_MS = 5_000;
|
|
99
|
+
const LIVE_STALE_MS = 1_500;
|
|
100
|
+
const LIVE_MIN_DURATION_MS = 250;
|
|
104
101
|
function estimateTokens(bytes, bytesPerToken) {
|
|
105
102
|
return Math.ceil(bytes / bytesPerToken);
|
|
106
103
|
}
|
|
@@ -123,10 +120,6 @@ export class TpsTracker {
|
|
|
123
120
|
// Insertion order is kept equal to run-start recency (see beginRun), which is
|
|
124
121
|
// what makes eviction from the front drop the stalest session.
|
|
125
122
|
runs = new Map();
|
|
126
|
-
// Calibrated bytes/token and the currently observed model step. Both share
|
|
127
|
-
// the bounded lifecycle of `runs`.
|
|
128
|
-
ratios = new Map();
|
|
129
|
-
calibrationSteps = new Map();
|
|
130
123
|
constructor(config = DEFAULT_CONFIG) {
|
|
131
124
|
this.config = config;
|
|
132
125
|
}
|
|
@@ -135,13 +128,12 @@ export class TpsTracker {
|
|
|
135
128
|
if (!st) {
|
|
136
129
|
st = {
|
|
137
130
|
phase: "ended",
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
toolPausedMs: 0,
|
|
131
|
+
settledTokens: 0,
|
|
132
|
+
settledDurationMs: 0,
|
|
133
|
+
tokensEstimated: false,
|
|
134
|
+
partial: false,
|
|
135
|
+
activeStep: null,
|
|
136
|
+
settledSteps: new Set(),
|
|
145
137
|
frozen: null
|
|
146
138
|
};
|
|
147
139
|
this.runs.set(sessionID, st);
|
|
@@ -150,15 +142,13 @@ export class TpsTracker {
|
|
|
150
142
|
}
|
|
151
143
|
beginRun(sessionID) {
|
|
152
144
|
const st = this.state(sessionID);
|
|
153
|
-
this.calibrationSteps.delete(sessionID);
|
|
154
145
|
st.phase = "running";
|
|
155
|
-
st.
|
|
156
|
-
st.
|
|
157
|
-
st.
|
|
158
|
-
st.
|
|
159
|
-
st.
|
|
160
|
-
st.
|
|
161
|
-
st.toolPausedMs = 0;
|
|
146
|
+
st.settledTokens = 0;
|
|
147
|
+
st.settledDurationMs = 0;
|
|
148
|
+
st.tokensEstimated = false;
|
|
149
|
+
st.partial = false;
|
|
150
|
+
st.activeStep = null;
|
|
151
|
+
st.settledSteps.clear();
|
|
162
152
|
st.frozen = null;
|
|
163
153
|
// Re-insert so this session becomes the newest in iteration order. Every
|
|
164
154
|
// entry is created through here, so the cap is checked on the one path that
|
|
@@ -175,159 +165,167 @@ export class TpsTracker {
|
|
|
175
165
|
this.dropSession(sessionID);
|
|
176
166
|
}
|
|
177
167
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (
|
|
168
|
+
ensureStep(sessionID, assistantMessageID, now, replace = false) {
|
|
169
|
+
const st = this.state(sessionID);
|
|
170
|
+
if (st.settledSteps.has(assistantMessageID) || st.phase === "ended" && st.frozen !== null) return null;
|
|
171
|
+
if (st.phase !== "running") this.beginRun(sessionID);
|
|
172
|
+
const running = this.state(sessionID);
|
|
173
|
+
if (running.activeStep?.assistantMessageID === assistantMessageID) return running.activeStep;
|
|
174
|
+
if (running.activeStep && !replace) return null;
|
|
175
|
+
if (running.activeStep) this.settleActiveStep(running, undefined);
|
|
176
|
+
const step = {
|
|
177
|
+
assistantMessageID,
|
|
178
|
+
startedAt: now,
|
|
179
|
+
lastBoundaryAt: null,
|
|
180
|
+
observableBytes: 0,
|
|
181
|
+
blocks: new Map(),
|
|
182
|
+
samples: []
|
|
183
|
+
};
|
|
184
|
+
running.activeStep = step;
|
|
185
|
+
running.frozen = null;
|
|
186
|
+
return step;
|
|
185
187
|
}
|
|
186
|
-
|
|
187
|
-
if (!delta) return;
|
|
188
|
+
beginStep(sessionID, assistantMessageID, now = Date.now()) {
|
|
188
189
|
const st = this.state(sessionID);
|
|
189
|
-
if (st.phase !== "running")
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
// that emit 1-3 byte deltas are not rounded up on every single one.
|
|
193
|
-
const bytes = Buffer.byteLength(delta, "utf8");
|
|
194
|
-
st.bytes += bytes;
|
|
195
|
-
const calibration = this.calibrationSteps.get(sessionID);
|
|
196
|
-
if (calibration && calibration.assistantMessageID === assistantMessageID) calibration.bytes += bytes;
|
|
197
|
-
const stepID = assistantMessageID ?? "";
|
|
198
|
-
if (st.activeAssistantMessageID !== stepID) {
|
|
199
|
-
st.activeAssistantMessageID = stepID;
|
|
200
|
-
st.lastSampleAt = null;
|
|
201
|
-
st.activeTools.clear();
|
|
202
|
-
st.toolPauseStartedAt = null;
|
|
203
|
-
st.toolPausedMs = 0;
|
|
204
|
-
}
|
|
205
|
-
if (st.lastSampleAt === null) {
|
|
206
|
-
st.lastSampleAt = now;
|
|
207
|
-
if (st.activeTools.size > 0) st.toolPauseStartedAt = now;
|
|
208
|
-
} else {
|
|
209
|
-
this.advanceTiming(st, now);
|
|
190
|
+
if (st.phase !== "running") {
|
|
191
|
+
if (st.settledSteps.has(assistantMessageID)) return;
|
|
192
|
+
this.beginRun(sessionID);
|
|
210
193
|
}
|
|
194
|
+
if (st.activeStep?.assistantMessageID === assistantMessageID) return;
|
|
195
|
+
this.ensureStep(sessionID, assistantMessageID, now, true);
|
|
211
196
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
197
|
+
beginBlock(sessionID, assistantMessageID, blockID, now) {
|
|
198
|
+
const step = this.ensureStep(sessionID, assistantMessageID, now);
|
|
199
|
+
if (!step) return;
|
|
200
|
+
if (!step.blocks.has(blockID)) step.blocks.set(blockID, {
|
|
201
|
+
streamedBytes: 0,
|
|
202
|
+
finalBytes: null
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
push(sessionID, delta, now, assistantMessageID = "implicit", blockID = "implicit") {
|
|
206
|
+
if (!delta) return;
|
|
207
|
+
const step = this.ensureStep(sessionID, assistantMessageID, now);
|
|
208
|
+
if (!step) return;
|
|
209
|
+
let block = step.blocks.get(blockID);
|
|
210
|
+
if (!block) {
|
|
211
|
+
block = {
|
|
212
|
+
streamedBytes: 0,
|
|
213
|
+
finalBytes: null
|
|
214
|
+
};
|
|
215
|
+
step.blocks.set(blockID, block);
|
|
224
216
|
}
|
|
225
|
-
if (
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
217
|
+
if (block.finalBytes !== null) return;
|
|
218
|
+
const bytes = Buffer.byteLength(delta, "utf8");
|
|
219
|
+
block.streamedBytes += bytes;
|
|
220
|
+
step.observableBytes += bytes;
|
|
221
|
+
step.samples.push({
|
|
222
|
+
bytes,
|
|
223
|
+
timestamp: now
|
|
229
224
|
});
|
|
225
|
+
const oldest = now - LIVE_WINDOW_MS;
|
|
226
|
+
while (step.samples[0] && step.samples[0].timestamp < oldest) step.samples.shift();
|
|
230
227
|
}
|
|
231
|
-
|
|
232
|
-
/** Pair a completed step's usage with deltas from that assistant message. */
|
|
233
|
-
finishStep(sessionID, assistantMessageID, generatedTokens, now) {
|
|
228
|
+
finishBlock(sessionID, assistantMessageID, blockID, text, now) {
|
|
234
229
|
const st = this.runs.get(sessionID);
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (!calibration || calibration.assistantMessageID !== assistantMessageID) return;
|
|
245
|
-
this.calibrationSteps.delete(sessionID);
|
|
246
|
-
if (this.ratios.has(sessionID)) return;
|
|
247
|
-
if (generatedTokens === undefined || !Number.isFinite(generatedTokens) || generatedTokens < CALIBRATION_MIN_TOKENS) return;
|
|
248
|
-
const bytes = calibration.bytes;
|
|
249
|
-
if (bytes < CALIBRATION_MIN_BYTES) return;
|
|
250
|
-
const ratio = bytes / generatedTokens;
|
|
251
|
-
if (ratio < BYTES_PER_TOKEN_MIN || ratio > BYTES_PER_TOKEN_MAX) {
|
|
252
|
-
mark(`calibration rejected sid=${sessionID} bytes=${bytes} tokens=${generatedTokens} ratio=${ratio.toFixed(2)}`);
|
|
253
|
-
return;
|
|
230
|
+
const step = st?.activeStep;
|
|
231
|
+
if (!step || step.assistantMessageID !== assistantMessageID) return;
|
|
232
|
+
let block = step.blocks.get(blockID);
|
|
233
|
+
if (!block) {
|
|
234
|
+
block = {
|
|
235
|
+
streamedBytes: 0,
|
|
236
|
+
finalBytes: null
|
|
237
|
+
};
|
|
238
|
+
step.blocks.set(blockID, block);
|
|
254
239
|
}
|
|
255
|
-
|
|
256
|
-
|
|
240
|
+
if (block.finalBytes !== null) return;
|
|
241
|
+
block.finalBytes = Buffer.byteLength(text, "utf8");
|
|
242
|
+
step.observableBytes += block.finalBytes - block.streamedBytes;
|
|
243
|
+
step.lastBoundaryAt = Math.max(step.lastBoundaryAt ?? now, now);
|
|
257
244
|
}
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
if (!
|
|
261
|
-
|
|
262
|
-
st.
|
|
245
|
+
settleActiveStep(st, generatedTokens) {
|
|
246
|
+
const step = st.activeStep;
|
|
247
|
+
if (!step) return;
|
|
248
|
+
const exact = generatedTokens !== undefined && Number.isFinite(generatedTokens) && generatedTokens >= 0;
|
|
249
|
+
st.settledTokens += exact ? generatedTokens : estimateTokens(step.observableBytes, this.config.bytesPerToken);
|
|
250
|
+
if (!exact) {
|
|
251
|
+
st.tokensEstimated = true;
|
|
252
|
+
st.partial = true;
|
|
253
|
+
}
|
|
254
|
+
if (step.lastBoundaryAt !== null) st.settledDurationMs += Math.max(0, step.lastBoundaryAt - step.startedAt);
|
|
255
|
+
st.settledSteps.add(step.assistantMessageID);
|
|
256
|
+
st.activeStep = null;
|
|
263
257
|
}
|
|
264
|
-
|
|
258
|
+
finishStep(sessionID, assistantMessageID, generatedTokens, _now) {
|
|
265
259
|
const st = this.runs.get(sessionID);
|
|
266
|
-
if (st?.
|
|
267
|
-
|
|
268
|
-
st.toolPauseStartedAt = null;
|
|
260
|
+
if (st?.activeStep?.assistantMessageID !== assistantMessageID) return;
|
|
261
|
+
this.settleActiveStep(st, generatedTokens);
|
|
269
262
|
}
|
|
270
|
-
|
|
271
|
-
/** The session's calibrated ratio, or the configured fallback. */
|
|
272
|
-
ratioFor(sessionID) {
|
|
273
|
-
return this.ratios.get(sessionID) ?? this.config.bytesPerToken;
|
|
274
|
-
}
|
|
275
|
-
finish(sessionID, now) {
|
|
263
|
+
finish(sessionID, _now) {
|
|
276
264
|
const st = this.runs.get(sessionID);
|
|
277
265
|
if (!st || st.phase === "ended") return;
|
|
278
|
-
this.
|
|
266
|
+
if (st.activeStep) this.settleActiveStep(st, undefined);
|
|
279
267
|
st.phase = "ended";
|
|
280
|
-
|
|
281
|
-
st.lastSampleAt = null;
|
|
282
|
-
st.activeAssistantMessageID = null;
|
|
283
|
-
st.activeTools.clear();
|
|
284
|
-
st.toolPauseStartedAt = null;
|
|
285
|
-
st.toolPausedMs = 0;
|
|
286
|
-
const tokens = estimateTokens(st.bytes, this.ratioFor(sessionID));
|
|
268
|
+
const tokens = st.settledTokens;
|
|
287
269
|
if (tokens <= 0) {
|
|
288
270
|
this.evictStale();
|
|
289
271
|
return;
|
|
290
272
|
}
|
|
291
|
-
const
|
|
273
|
+
const tps = st.settledDurationMs > 0 ? tokens / (st.settledDurationMs / 1000) : null;
|
|
292
274
|
st.frozen = {
|
|
293
|
-
tps
|
|
294
|
-
tokens
|
|
275
|
+
tps,
|
|
276
|
+
tokens,
|
|
277
|
+
tokensEstimated: st.tokensEstimated,
|
|
278
|
+
partial: st.partial
|
|
295
279
|
};
|
|
296
|
-
mark(`finish sid=${sessionID} tokens=${tokens}
|
|
280
|
+
mark(`finish sid=${sessionID} tokens=${tokens} observedMs=${st.settledDurationMs} tps=${tps?.toFixed(1) ?? "n/a"}`);
|
|
297
281
|
this.evictStale();
|
|
298
282
|
}
|
|
299
283
|
dropSession(sessionID) {
|
|
300
284
|
this.runs.delete(sessionID);
|
|
301
|
-
this.ratios.delete(sessionID);
|
|
302
|
-
this.calibrationSteps.delete(sessionID);
|
|
303
285
|
}
|
|
304
286
|
evict(sessionID) {
|
|
305
287
|
this.dropSession(sessionID);
|
|
306
288
|
}
|
|
307
|
-
hasRunning() {
|
|
289
|
+
hasRunning(now = Date.now()) {
|
|
308
290
|
for (const st of this.runs.values()) {
|
|
309
|
-
|
|
291
|
+
const last = st.activeStep?.samples.at(-1);
|
|
292
|
+
if (st.phase === "running" && last && now < last.timestamp + LIVE_STALE_MS) return true;
|
|
310
293
|
}
|
|
311
294
|
return false;
|
|
312
295
|
}
|
|
296
|
+
liveTps(step, now) {
|
|
297
|
+
const last = step.samples.at(-1);
|
|
298
|
+
if (!last) return null;
|
|
299
|
+
const effectiveNow = Math.min(now, last.timestamp + LIVE_STALE_MS);
|
|
300
|
+
const oldest = effectiveNow - LIVE_WINDOW_MS;
|
|
301
|
+
const samples = step.samples.filter(sample => sample.timestamp >= oldest);
|
|
302
|
+
const first = samples[0];
|
|
303
|
+
if (!first) return null;
|
|
304
|
+
const bytes = samples.reduce((total, sample) => total + sample.bytes, 0);
|
|
305
|
+
const durationMs = Math.max(effectiveNow - first.timestamp, LIVE_MIN_DURATION_MS);
|
|
306
|
+
return estimateTokens(bytes, this.config.bytesPerToken) / (durationMs / 1000);
|
|
307
|
+
}
|
|
313
308
|
value(sessionID, now) {
|
|
314
309
|
const st = this.runs.get(sessionID);
|
|
315
310
|
if (!st) return null;
|
|
316
311
|
if (st.frozen) return {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
312
|
+
...st.frozen,
|
|
313
|
+
frozen: true,
|
|
314
|
+
tpsEstimated: true
|
|
320
315
|
};
|
|
321
316
|
if (st.phase !== "running") return null;
|
|
322
|
-
const
|
|
317
|
+
const active = st.activeStep;
|
|
318
|
+
const activeTokens = active ? estimateTokens(active.observableBytes, this.config.bytesPerToken) : 0;
|
|
319
|
+
const tokens = st.settledTokens + activeTokens;
|
|
323
320
|
if (tokens <= 0) return null;
|
|
324
|
-
const
|
|
325
|
-
const activeTail = st.lastSampleAt === null ? 0 : Math.max(0, now - st.lastSampleAt - st.toolPausedMs - currentPause);
|
|
326
|
-
const seconds = Math.max(st.activeMs + activeTail, 1) / 1000;
|
|
321
|
+
const settledTps = st.settledDurationMs > 0 ? st.settledTokens / (st.settledDurationMs / 1000) : null;
|
|
327
322
|
return {
|
|
328
|
-
tps:
|
|
323
|
+
tps: active ? this.liveTps(active, now) ?? settledTps : settledTps,
|
|
329
324
|
tokens,
|
|
330
|
-
frozen: false
|
|
325
|
+
frozen: false,
|
|
326
|
+
tokensEstimated: st.tokensEstimated || active !== null,
|
|
327
|
+
tpsEstimated: true,
|
|
328
|
+
partial: st.partial
|
|
331
329
|
};
|
|
332
330
|
}
|
|
333
331
|
}
|
|
@@ -373,9 +371,11 @@ export function resolveOptions(raw) {
|
|
|
373
371
|
};
|
|
374
372
|
}
|
|
375
373
|
export function formatLabel(value, display) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
374
|
+
const tokens = `${value.tokensEstimated ? "~" : ""}${value.tokens} tok`;
|
|
375
|
+
const tps = value.tps === null ? null : `~${formatTps(value.tps)} t/s`;
|
|
376
|
+
if (display === "tokens") return tokens;
|
|
377
|
+
if (display === "tps") return tps ?? "— t/s";
|
|
378
|
+
return tps === null ? tokens : `${tokens} · ${tps}`;
|
|
379
379
|
}
|
|
380
380
|
|
|
381
381
|
// ---------------------------------------------------------------------------
|
|
@@ -385,6 +385,10 @@ export function formatLabel(value, display) {
|
|
|
385
385
|
// `data.listen` signature) rather than restated structurally: handlers are
|
|
386
386
|
// contravariant, so hand-written shapes keep typechecking after a field rename.
|
|
387
387
|
|
|
388
|
+
function blockID(e) {
|
|
389
|
+
if (e.type === "session.tool.input.delta" || e.type === "session.tool.input.started" || e.type === "session.tool.input.ended") return `tool:${e.data.id}`;
|
|
390
|
+
return `${e.type.startsWith("session.text.") ? "text" : "reasoning"}:${e.data.ordinal}`;
|
|
391
|
+
}
|
|
388
392
|
const definition = {
|
|
389
393
|
id: "opencode2.tps",
|
|
390
394
|
setup(ctx) {
|
|
@@ -406,6 +410,16 @@ const definition = {
|
|
|
406
410
|
configureDebug(options.debug || isEnvEnabled(process.env["TPS_DEBUG"]));
|
|
407
411
|
const tracker = new TpsTracker(options);
|
|
408
412
|
const [version, setVersion] = createSignal(0);
|
|
413
|
+
const seenEventIDs = new Set();
|
|
414
|
+
const isNewEvent = e => {
|
|
415
|
+
if (seenEventIDs.has(e.id)) return false;
|
|
416
|
+
seenEventIDs.add(e.id);
|
|
417
|
+
if (seenEventIDs.size > 4_096) {
|
|
418
|
+
const oldest = seenEventIDs.values().next().value;
|
|
419
|
+
if (oldest !== undefined) seenEventIDs.delete(oldest);
|
|
420
|
+
}
|
|
421
|
+
return true;
|
|
422
|
+
};
|
|
409
423
|
mark(`setup ok app=${ctx.app.version} gen=${mine} display=${options.display} refreshHz=${options.refreshHz}`);
|
|
410
424
|
|
|
411
425
|
// Rendering is throttled: deltas arrive at 100-200/s, and every bump costs
|
|
@@ -420,9 +434,9 @@ const definition = {
|
|
|
420
434
|
stopTimer();
|
|
421
435
|
return;
|
|
422
436
|
}
|
|
423
|
-
const running = tracker.hasRunning();
|
|
424
|
-
//
|
|
425
|
-
//
|
|
437
|
+
const running = tracker.hasRunning(Date.now());
|
|
438
|
+
// The observable live rate decays only through a short stale tail. Opaque
|
|
439
|
+
// provider work after that is not charged to a numerator we cannot see.
|
|
426
440
|
if (dirty || running) {
|
|
427
441
|
dirty = false;
|
|
428
442
|
setVersion(v => v + 1);
|
|
@@ -441,42 +455,42 @@ const definition = {
|
|
|
441
455
|
timer.unref?.();
|
|
442
456
|
};
|
|
443
457
|
const onDelta = e => {
|
|
444
|
-
if (!isActive()) return;
|
|
445
|
-
tracker.push(e.data.sessionID, e.data.delta,
|
|
458
|
+
if (!isActive() || !isNewEvent(e)) return;
|
|
459
|
+
tracker.push(e.data.sessionID, e.data.delta, e.created, e.data.assistantMessageID, blockID(e));
|
|
460
|
+
touch();
|
|
461
|
+
};
|
|
462
|
+
const onBlockStarted = e => {
|
|
463
|
+
if (!isActive() || !isNewEvent(e)) return;
|
|
464
|
+
tracker.beginBlock(e.data.sessionID, e.data.assistantMessageID, blockID(e), e.created);
|
|
465
|
+
};
|
|
466
|
+
const onBlockEnded = e => {
|
|
467
|
+
if (!isActive() || !isNewEvent(e)) return;
|
|
468
|
+
tracker.finishBlock(e.data.sessionID, e.data.assistantMessageID, blockID(e), e.data.text, e.created);
|
|
446
469
|
touch();
|
|
447
470
|
};
|
|
448
471
|
const onFinish = e => {
|
|
449
|
-
if (!isActive()) return;
|
|
450
|
-
tracker.finish(e.data.sessionID,
|
|
472
|
+
if (!isActive() || !isNewEvent(e)) return;
|
|
473
|
+
tracker.finish(e.data.sessionID, e.created);
|
|
451
474
|
touch();
|
|
452
475
|
};
|
|
453
476
|
const onStepStarted = e => {
|
|
454
|
-
if (!isActive()) return;
|
|
455
|
-
tracker.beginStep(e.data.sessionID, e.data.assistantMessageID);
|
|
477
|
+
if (!isActive() || !isNewEvent(e)) return;
|
|
478
|
+
tracker.beginStep(e.data.sessionID, e.data.assistantMessageID, e.created);
|
|
479
|
+
touch();
|
|
456
480
|
};
|
|
457
481
|
const onStepFinished = e => {
|
|
458
|
-
if (!isActive()) return;
|
|
482
|
+
if (!isActive() || !isNewEvent(e)) return;
|
|
459
483
|
const tokens = e.data.tokens;
|
|
460
|
-
|
|
484
|
+
const generatedTokens = tokens !== undefined && Number.isFinite(tokens.output) && tokens.output >= 0 && Number.isFinite(tokens.reasoning) && tokens.reasoning >= 0 ? tokens.output + tokens.reasoning : undefined;
|
|
485
|
+
tracker.finishStep(e.data.sessionID, e.data.assistantMessageID, generatedTokens, e.created);
|
|
461
486
|
touch();
|
|
462
487
|
};
|
|
463
|
-
const onToolStarted = e => {
|
|
464
|
-
if (!isActive()) return;
|
|
465
|
-
tracker.beginTool(e.data.sessionID, e.data.assistantMessageID, e.data.id, Date.now());
|
|
466
|
-
};
|
|
467
|
-
const onToolFinished = e => {
|
|
468
|
-
if (!isActive()) return;
|
|
469
|
-
tracker.finishTool(e.data.sessionID, e.data.assistantMessageID, e.data.id, Date.now());
|
|
470
|
-
};
|
|
471
488
|
const unsubs = [ctx.data.on("session.execution.started", e => {
|
|
472
|
-
if (!isActive()) return;
|
|
489
|
+
if (!isActive() || !isNewEvent(e)) return;
|
|
473
490
|
tracker.beginRun(e.data.sessionID);
|
|
474
491
|
touch();
|
|
475
|
-
}), ctx.data.on("session.text.delta", onDelta), ctx.data.on("session.reasoning.delta", onDelta),
|
|
476
|
-
|
|
477
|
-
// partway through every large write/edit while generation is at full rate.
|
|
478
|
-
ctx.data.on("session.tool.input.delta", onDelta), ctx.data.on("session.step.started", onStepStarted), ctx.data.on("session.step.ended", onStepFinished), ctx.data.on("session.step.failed", onStepFinished), ctx.data.on("session.tool.called", onToolStarted), ctx.data.on("session.tool.success", onToolFinished), ctx.data.on("session.tool.failed", onToolFinished), ctx.data.on("session.execution.succeeded", onFinish), ctx.data.on("session.execution.failed", onFinish), ctx.data.on("session.execution.interrupted", onFinish), ctx.data.on("session.idle", onFinish), ctx.data.on("session.deleted", e => {
|
|
479
|
-
if (!isActive()) return;
|
|
492
|
+
}), ctx.data.on("session.text.delta", onDelta), ctx.data.on("session.reasoning.delta", onDelta), ctx.data.on("session.tool.input.delta", onDelta), ctx.data.on("session.text.started", onBlockStarted), ctx.data.on("session.reasoning.started", onBlockStarted), ctx.data.on("session.tool.input.started", onBlockStarted), ctx.data.on("session.text.ended", onBlockEnded), ctx.data.on("session.reasoning.ended", onBlockEnded), ctx.data.on("session.tool.input.ended", onBlockEnded), ctx.data.on("session.step.started", onStepStarted), ctx.data.on("session.step.ended", onStepFinished), ctx.data.on("session.step.failed", onStepFinished), ctx.data.on("session.execution.succeeded", onFinish), ctx.data.on("session.execution.failed", onFinish), ctx.data.on("session.execution.interrupted", onFinish), ctx.data.on("session.idle", onFinish), ctx.data.on("session.deleted", e => {
|
|
493
|
+
if (!isActive() || !isNewEvent(e)) return;
|
|
480
494
|
tracker.evict(e.data.sessionID);
|
|
481
495
|
touch();
|
|
482
496
|
})];
|