oc-metricboard 0.1.6 → 0.1.8
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 +16 -3
- package/src/session-hydration.ts +5 -0
- package/src/turn-state.ts +26 -1
- 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,
|
|
3
|
+
import { getDisplayInputTokens, getDisplayOutputTokens, 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,15 @@ export function createCollector(
|
|
|
352
352
|
|
|
353
353
|
const cacheReadCompleteness: CacheReadCompleteness =
|
|
354
354
|
exactCacheCount === 0 ? "unknown" : exactCacheCount === metrics.length ? "exact" : "partial"
|
|
355
|
-
|
|
355
|
+
// Model rows use the same turn-level TTFT as the main aggregate: the
|
|
356
|
+
// earliest gated turn TTFT across the group's sessions, falling back to
|
|
357
|
+
// per-request TTFT when no turn timing exists (e.g. sub-agent sessions
|
|
358
|
+
// whose turn start coincides with their first request).
|
|
359
|
+
let ttft: number | null = null
|
|
360
|
+
for (const m of metrics) {
|
|
361
|
+
const candidate = getTurnTtft(state.turns.get(m.sessionID), getTtft(m))
|
|
362
|
+
if (candidate !== null && (ttft === null || candidate < ttft)) ttft = candidate
|
|
363
|
+
}
|
|
356
364
|
|
|
357
365
|
// Live TPS for this model group
|
|
358
366
|
let liveTps = 0
|
|
@@ -546,7 +554,12 @@ export function createCollector(
|
|
|
546
554
|
|
|
547
555
|
// NEW: Build per-model breakdown for tree scope
|
|
548
556
|
const modelBreakdown = scope === "tree" ? aggregateByModel(ids, now, scope) : []
|
|
549
|
-
|
|
557
|
+
// Turn-level TTFT: opencode stamps step.started at the first token, so
|
|
558
|
+
// intra-turn steps cannot measure their own request start (they would
|
|
559
|
+
// collapse to ~0). Anchor TTFT to the turn's user message instead —
|
|
560
|
+
// stable across all steps within a turn, refreshed on each new turn.
|
|
561
|
+
// Falls back to the per-request measurement when no turn timing exists.
|
|
562
|
+
const ttft = getTurnTtft(foregroundTurn, foregroundRequest ? getTtft(foregroundRequest) : null)
|
|
550
563
|
|
|
551
564
|
const result: MetricsAggregate = {
|
|
552
565
|
sessionIDs: contributingSessionIDs.length > 0 ? contributingSessionIDs : [rootID],
|
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
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { getDisplayInputTokens, getDisplayOutputTokens } from "./metrics"
|
|
1
|
+
import { gateTtft, getDisplayInputTokens, getDisplayOutputTokens } from "./metrics"
|
|
2
2
|
import type { RequestMetrics, TurnMetrics } from "./types"
|
|
3
3
|
|
|
4
4
|
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,27 @@ 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
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Turn-level TTFT (user message → first token), gated. Falls back to the
|
|
100
|
+
* supplied per-request TTFT when the turn has no first-token timing. This is
|
|
101
|
+
* the ONLY meaningful TTFT observable from opencode's event stream for
|
|
102
|
+
* intra-turn steps, so every TTFT display path should prefer it.
|
|
103
|
+
*/
|
|
104
|
+
export function getTurnTtft(turn: TurnMetrics | undefined, fallback: number | null = null): number | null {
|
|
105
|
+
if (turn?.firstTokenTime != null) {
|
|
106
|
+
const gated = gateTtft(turn.firstTokenTime - turn.turnStartTime)
|
|
107
|
+
if (gated !== null) return gated
|
|
108
|
+
}
|
|
109
|
+
return fallback
|
|
110
|
+
}
|
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>
|