oc-metricboard 0.1.5 → 0.1.7
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/assistant-progress.ts +2 -0
- package/src/collector.ts +11 -3
- package/src/metrics.ts +12 -4
- package/src/request-state.ts +12 -7
- package/src/session-hydration.ts +5 -0
- package/src/turn-state.ts +11 -0
- package/src/types.ts +1 -0
package/package.json
CHANGED
|
@@ -4,6 +4,7 @@ import { currentRequest } from "./request-state"
|
|
|
4
4
|
import { eventAggregateID, eventID, eventProperties, stringEventProperty } from "./event-bus"
|
|
5
5
|
import type { EventHandlerContext } from "./collector-state"
|
|
6
6
|
import { recordLiveTokens } from "./live-speed"
|
|
7
|
+
import { ensureTurn, recordTurnFirstToken } from "./turn-state"
|
|
7
8
|
|
|
8
9
|
export interface AssistantProgressPart {
|
|
9
10
|
readonly partID: string
|
|
@@ -26,6 +27,7 @@ function applyTokenDelta(
|
|
|
26
27
|
if (state.turns.get(sessionID)?.finalizedSteps.has(messageID)) return
|
|
27
28
|
const current = currentRequest({ state, actions, sessionID, messageID, now })
|
|
28
29
|
if (current.firstTokenTime === null) current.firstTokenTime = now
|
|
30
|
+
recordTurnFirstToken(ensureTurn(state.turns, sessionID, now), now)
|
|
29
31
|
if (deltaTokens > 0) {
|
|
30
32
|
current.estimatedOutputTokens += deltaTokens
|
|
31
33
|
current.lastDeltaTime = now
|
package/src/collector.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { TuiPluginApi } from "@opencode-ai/plugin/tui"
|
|
2
2
|
import type { BarConfig, CacheReadCompleteness, MetricsAggregate, MetricsScope, ModelMetrics, RequestMetrics } from "./types"
|
|
3
|
-
import { getDisplayInputTokens, getDisplayOutputTokens, getTtft } from "./metrics"
|
|
3
|
+
import { getDisplayInputTokens, getDisplayOutputTokens, gateTtft, getTtft } from "./metrics"
|
|
4
4
|
import { registerEventHandlers } from "./event-handlers"
|
|
5
5
|
import type { CollectorState } from "./collector-state"
|
|
6
6
|
import type { MetricsEventApi } from "./event-bus"
|
|
@@ -352,7 +352,7 @@ export function createCollector(
|
|
|
352
352
|
|
|
353
353
|
const cacheReadCompleteness: CacheReadCompleteness =
|
|
354
354
|
exactCacheCount === 0 ? "unknown" : exactCacheCount === metrics.length ? "exact" : "partial"
|
|
355
|
-
const ttft = firstTokenTime === null ? null :
|
|
355
|
+
const ttft = firstTokenTime === null ? null : gateTtft(firstTokenTime - requestStartTime)
|
|
356
356
|
|
|
357
357
|
// Live TPS for this model group
|
|
358
358
|
let liveTps = 0
|
|
@@ -546,7 +546,15 @@ export function createCollector(
|
|
|
546
546
|
|
|
547
547
|
// NEW: Build per-model breakdown for tree scope
|
|
548
548
|
const modelBreakdown = scope === "tree" ? aggregateByModel(ids, now, scope) : []
|
|
549
|
-
|
|
549
|
+
// Turn-level TTFT: opencode stamps step.started at the first token, so
|
|
550
|
+
// intra-turn steps cannot measure their own request start (they would
|
|
551
|
+
// collapse to ~0). Anchor TTFT to the turn's user message instead —
|
|
552
|
+
// stable across all steps within a turn, refreshed on each new turn.
|
|
553
|
+
// Falls back to the per-request measurement when no turn timing exists.
|
|
554
|
+
const turnTtft = foregroundTurn?.firstTokenTime != null
|
|
555
|
+
? gateTtft(foregroundTurn.firstTokenTime - foregroundTurn.turnStartTime)
|
|
556
|
+
: null
|
|
557
|
+
const ttft = turnTtft ?? (foregroundRequest ? getTtft(foregroundRequest) : null)
|
|
550
558
|
|
|
551
559
|
const result: MetricsAggregate = {
|
|
552
560
|
sessionIDs: contributingSessionIDs.length > 0 ? contributingSessionIDs : [rootID],
|
package/src/metrics.ts
CHANGED
|
@@ -81,7 +81,7 @@ export function aggregateRequestMetrics(
|
|
|
81
81
|
|
|
82
82
|
const cacheReadCompleteness: CacheReadCompleteness =
|
|
83
83
|
exactCacheCount === 0 ? "unknown" : exactCacheCount === metrics.length ? "exact" : "partial"
|
|
84
|
-
const ttft = firstTokenTime === null ? null :
|
|
84
|
+
const ttft = firstTokenTime === null ? null : gateTtft(firstTokenTime - requestStartTime)
|
|
85
85
|
const sessionIDs = [...new Set(metrics.map((m) => m.sessionID))]
|
|
86
86
|
|
|
87
87
|
return {
|
|
@@ -98,6 +98,7 @@ export function aggregateRequestMetrics(
|
|
|
98
98
|
liveTps: null,
|
|
99
99
|
isStreaming,
|
|
100
100
|
isComplete,
|
|
101
|
+
modelBreakdown: [],
|
|
101
102
|
}
|
|
102
103
|
}
|
|
103
104
|
|
|
@@ -112,11 +113,18 @@ export function aggregateRequestMetrics(
|
|
|
112
113
|
*/
|
|
113
114
|
export const MAX_TTFT_MS = 10 * 60 * 1000
|
|
114
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Gate a raw TTFT value: non-finite, negative, or beyond MAX_TTFT_MS is not a
|
|
118
|
+
* meaningful TTFT and is reported as null (rendered "--").
|
|
119
|
+
*/
|
|
120
|
+
export function gateTtft(rawMs: number): number | null {
|
|
121
|
+
if (!Number.isFinite(rawMs) || rawMs < 0 || rawMs > MAX_TTFT_MS) return null
|
|
122
|
+
return Math.round(rawMs)
|
|
123
|
+
}
|
|
124
|
+
|
|
115
125
|
export function getTtft(m: RequestMetrics): number | null {
|
|
116
126
|
if (m.firstTokenTime === null) return null
|
|
117
|
-
|
|
118
|
-
if (!Number.isFinite(ttft) || ttft < 0 || ttft > MAX_TTFT_MS) return null
|
|
119
|
-
return Math.round(ttft)
|
|
127
|
+
return gateTtft(m.firstTokenTime - m.requestStartTime)
|
|
120
128
|
}
|
|
121
129
|
|
|
122
130
|
/**
|
package/src/request-state.ts
CHANGED
|
@@ -38,13 +38,18 @@ export function currentRequest(input: CurrentRequestInput): RequestMetrics {
|
|
|
38
38
|
? startTurn(input.state.turns, input.sessionID, input.now)
|
|
39
39
|
: ensureTurn(input.state.turns, input.sessionID, existing?.requestStartTime ?? input.now)
|
|
40
40
|
if (existing && isDifferentMessage && !startsAfterCompletedTurn) retireRequestIntoTurn(turn, existing)
|
|
41
|
-
// Keep the existing request's start time
|
|
42
|
-
// opencode 1.18.x stamps
|
|
43
|
-
//
|
|
44
|
-
// the correct "request submitted" anchor.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
// Keep the existing request's start time ONLY while the existing request
|
|
42
|
+
// is still an un-streamed placeholder (opencode 1.18.x stamps
|
|
43
|
+
// step.started lazily at the first token, so the user message's created
|
|
44
|
+
// time is the correct "request submitted" anchor). Once the previous
|
|
45
|
+
// request has produced a first token it was a real generation, and a new
|
|
46
|
+
// messageID is a NEW step: it must NOT inherit the turn's original start
|
|
47
|
+
// time, otherwise TTFT absorbs every tool execution since the turn began
|
|
48
|
+
// (huge TTFT in long agentic sessions).
|
|
49
|
+
const inheritAnchor = !startsAfterCompletedTurn && (existing?.firstTokenTime ?? null) === null
|
|
50
|
+
const requestStartTime = inheritAnchor
|
|
51
|
+
? (existing?.requestStartTime ?? input.now)
|
|
52
|
+
: input.now
|
|
48
53
|
const current = createFreshMetrics(
|
|
49
54
|
input.sessionID,
|
|
50
55
|
input.messageID,
|
package/src/session-hydration.ts
CHANGED
|
@@ -233,6 +233,11 @@ export async function hydrateSession(input: HydrateSessionInput): Promise<boolea
|
|
|
233
233
|
input.now,
|
|
234
234
|
) ?? requestStartTime
|
|
235
235
|
const turn = createTurnMetrics(input.sessionID, turnStartTime)
|
|
236
|
+
// Turn-level TTFT anchor: the first trailing assistant's createdTime is
|
|
237
|
+
// stamped at its first token, so turnStart → that instant is the hydrated
|
|
238
|
+
// turn's time-to-first-token. Fall back to the live request's first token.
|
|
239
|
+
turn.firstTokenTime = toPerformanceTime(trailingAssistants[0]?.createdTime, input.now)
|
|
240
|
+
?? current.firstTokenTime
|
|
236
241
|
for (const step of trailingAssistants) {
|
|
237
242
|
if (!step.tokens || !hasPositiveAssistantTokens(step.tokens)) continue
|
|
238
243
|
const stepStart = toPerformanceTime(step.createdTime, input.now) ?? turnStartTime
|
package/src/turn-state.ts
CHANGED
|
@@ -5,6 +5,7 @@ export function createTurnMetrics(sessionID: string, now: number): TurnMetrics {
|
|
|
5
5
|
return {
|
|
6
6
|
sessionID,
|
|
7
7
|
turnStartTime: now,
|
|
8
|
+
firstTokenTime: null,
|
|
8
9
|
completeTime: null,
|
|
9
10
|
finalizedOutputTokens: 0,
|
|
10
11
|
finalizedSteps: new Map(),
|
|
@@ -83,3 +84,13 @@ export function completeTurn(turn: TurnMetrics, now: number): void {
|
|
|
83
84
|
turn.isComplete = true
|
|
84
85
|
turn.completeTime = now
|
|
85
86
|
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Record the turn's first token instant (once). opencode stamps step.started
|
|
90
|
+
* at the first token, so intra-turn steps cannot measure their own request
|
|
91
|
+
* start; the turn-level anchor (user message → first delta) is the only
|
|
92
|
+
* meaningful TTFT observable from the event stream.
|
|
93
|
+
*/
|
|
94
|
+
export function recordTurnFirstToken(turn: TurnMetrics, now: number): void {
|
|
95
|
+
if (turn.firstTokenTime === null) turn.firstTokenTime = now
|
|
96
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -37,6 +37,7 @@ export interface FinalizedStepUsage {
|
|
|
37
37
|
export interface TurnMetrics {
|
|
38
38
|
sessionID: string
|
|
39
39
|
turnStartTime: number
|
|
40
|
+
firstTokenTime: number | null // First token of the turn (user message → first delta)
|
|
40
41
|
completeTime: number | null
|
|
41
42
|
finalizedOutputTokens: number
|
|
42
43
|
finalizedSteps: Map<string, FinalizedStepUsage>
|