@tonyclaw/agent-inspector 2.0.27 → 2.0.29
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/.output/cli.js +169 -13
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-B_p6vmMQ.js → CompareDrawer-DcqxlgSQ.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-D650kQlv.js +115 -0
- package/.output/public/assets/{ReplayDialog-BmprJ6D3.js → ReplayDialog-CBcnPgx1.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-DqO0_SVJ.js → RequestAnatomy-DSgSqCYt.js} +1 -1
- package/.output/public/assets/{ResponseView-FZbPNdHa.js → ResponseView-CjqzBSoF.js} +1 -1
- package/.output/public/assets/{StreamingChunkSequence-BZPzfJWj.js → StreamingChunkSequence-DdBBKolI.js} +1 -1
- package/.output/public/assets/_sessionId-wctOoGh_.js +1 -0
- package/.output/public/assets/index-BhQGSdhG.js +1 -0
- package/.output/public/assets/{index-DsiKfWCp.css → index-CwlHPmgL.css} +1 -1
- package/.output/public/assets/{main-CzItFZlM.js → main-B6OLZCp9.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +13 -13
- package/.output/server/{_sessionId-DZH8SrEZ.mjs → _sessionId-CCfKJJ19.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-L0aE1UV1.mjs → CompareDrawer-uaPI5R6e.mjs} +2 -2
- package/.output/server/_ssr/{ProxyViewerContainer-B62RB9ER.mjs → ProxyViewerContainer-BV2nIxXy.mjs} +83 -20
- package/.output/server/_ssr/{ReplayDialog-FVWnpx2C.mjs → ReplayDialog-eE2Hy2Nl.mjs} +4 -4
- package/.output/server/_ssr/{RequestAnatomy-DIyqW-Ny.mjs → RequestAnatomy-B7vwWo57.mjs} +2 -2
- package/.output/server/_ssr/{ResponseView-BX4mxEZ5.mjs → ResponseView-Bm6Qp8mo.mjs} +2 -2
- package/.output/server/_ssr/{StreamingChunkSequence-Cs3nzOor.mjs → StreamingChunkSequence-CbAJKJQx.mjs} +2 -2
- package/.output/server/_ssr/{index-Dik2Mc3h.mjs → index-u14_Aj60.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-DCPg8ykx.mjs → router-qgeGUp1k.mjs} +161 -18
- package/.output/server/{_tanstack-start-manifest_v-BaoL3JCh.mjs → _tanstack-start-manifest_v-BrfUXnre.mjs} +1 -1
- package/.output/server/index.mjs +59 -59
- package/package.json +1 -1
- package/src/cli/networkHints.ts +150 -0
- package/src/cli.ts +72 -13
- package/src/components/ProxyViewer.tsx +3 -0
- package/src/components/providers/ProviderCard.tsx +32 -0
- package/src/components/proxy-viewer/LogEntryHeader.tsx +38 -1
- package/src/lib/providerTestContract.ts +3 -0
- package/src/lib/sessionInfoContract.ts +3 -0
- package/src/mcp/toolHandlers.ts +3 -0
- package/src/proxy/evidenceAnalysis.ts +5 -0
- package/src/proxy/evidenceExporter.ts +8 -3
- package/src/proxy/handler.ts +8 -1
- package/src/proxy/logFinalizer.ts +36 -0
- package/src/proxy/schemas.ts +3 -0
- package/src/proxy/sessionInfo.ts +3 -0
- package/src/proxy/store.ts +3 -0
- package/src/proxy/streamTiming.ts +31 -0
- package/src/routes/api/logs.$id.replay.ts +25 -2
- package/src/routes/api/providers.$providerId.test.log.ts +34 -2
- package/.output/public/assets/ProxyViewerContainer-DItqh1EO.js +0 -115
- package/.output/public/assets/_sessionId-y2ATIp51.js +0 -1
- package/.output/public/assets/index-CQT4higx.js +0 -1
|
@@ -45,6 +45,9 @@ export const ProviderTestResultSchema = z.object({
|
|
|
45
45
|
cacheCreationInputTokens: z.number().optional(),
|
|
46
46
|
cacheReadInputTokens: z.number().optional(),
|
|
47
47
|
latencyMs: z.number().optional(),
|
|
48
|
+
firstChunkMs: z.number().nullable().optional(),
|
|
49
|
+
totalStreamMs: z.number().nullable().optional(),
|
|
50
|
+
tokensPerSecond: z.number().nullable().optional(),
|
|
48
51
|
content: z.array(ProviderTestContentSchema).optional(),
|
|
49
52
|
rawResponse: z.string().optional(),
|
|
50
53
|
streaming: z.boolean().optional(),
|
|
@@ -21,6 +21,9 @@ export const SessionLogSummarySchema = z.object({
|
|
|
21
21
|
hasError: z.boolean(),
|
|
22
22
|
error: z.string().nullable(),
|
|
23
23
|
latencyMs: z.number().nullable(),
|
|
24
|
+
firstChunkMs: z.number().nullable().optional(),
|
|
25
|
+
totalStreamMs: z.number().nullable().optional(),
|
|
26
|
+
tokensPerSecond: z.number().nullable().optional(),
|
|
24
27
|
tokens: z.object({
|
|
25
28
|
input: z.number().nullable(),
|
|
26
29
|
output: z.number().nullable(),
|
package/src/mcp/toolHandlers.ts
CHANGED
|
@@ -132,6 +132,9 @@ export function buildLogSummary(log: CapturedLog) {
|
|
|
132
132
|
isStreaming: log.streaming,
|
|
133
133
|
hasError,
|
|
134
134
|
latencyMs: log.elapsedMs,
|
|
135
|
+
firstChunkMs: log.firstChunkMs ?? null,
|
|
136
|
+
totalStreamMs: log.totalStreamMs ?? null,
|
|
137
|
+
tokensPerSecond: log.tokensPerSecond ?? null,
|
|
135
138
|
tokens: {
|
|
136
139
|
input: log.inputTokens,
|
|
137
140
|
output: log.outputTokens,
|
|
@@ -318,6 +318,11 @@ function logDetails(log: SessionLogSummary): string {
|
|
|
318
318
|
`streaming=${log.isStreaming ? "yes" : "no"}`,
|
|
319
319
|
`latencyMs=${formatNullable(log.latencyMs)}`,
|
|
320
320
|
];
|
|
321
|
+
if (log.isStreaming) {
|
|
322
|
+
parts.push(`firstChunkMs=${formatNullable(log.firstChunkMs ?? null)}`);
|
|
323
|
+
parts.push(`totalStreamMs=${formatNullable(log.totalStreamMs ?? null)}`);
|
|
324
|
+
parts.push(`tokensPerSecond=${formatNullable(log.tokensPerSecond ?? null)}`);
|
|
325
|
+
}
|
|
321
326
|
if (log.error !== null) {
|
|
322
327
|
parts.push(`error=${log.error}`);
|
|
323
328
|
}
|
|
@@ -112,14 +112,19 @@ function buildMarkdown(document: EvidenceDocument): string {
|
|
|
112
112
|
lines.push("No compact logs were available.", "");
|
|
113
113
|
} else {
|
|
114
114
|
lines.push(
|
|
115
|
-
"| ID | Status | Model | Latency | Path | Error |",
|
|
116
|
-
"| --- | --- | --- | --- | --- | --- |",
|
|
115
|
+
"| ID | Status | Model | Latency | Stream | Path | Error |",
|
|
116
|
+
"| --- | --- | --- | --- | --- | --- | --- |",
|
|
117
117
|
);
|
|
118
118
|
for (const log of session.latestLogs) {
|
|
119
|
+
const streamTiming = log.isStreaming
|
|
120
|
+
? `first=${log.firstChunkMs ?? "n/a"}ms total=${log.totalStreamMs ?? "n/a"}ms tps=${
|
|
121
|
+
log.tokensPerSecond ?? "n/a"
|
|
122
|
+
}`
|
|
123
|
+
: "n/a";
|
|
119
124
|
lines.push(
|
|
120
125
|
`| ${log.id} | ${log.status ?? "n/a"} | ${log.model ?? "n/a"} | ${
|
|
121
126
|
log.latencyMs ?? "n/a"
|
|
122
|
-
} | ${log.path} | ${log.hasError ? "yes" : "no"} |`,
|
|
127
|
+
} | ${streamTiming} | ${log.path} | ${log.hasError ? "yes" : "no"} |`,
|
|
123
128
|
);
|
|
124
129
|
}
|
|
125
130
|
lines.push("");
|
package/src/proxy/handler.ts
CHANGED
|
@@ -142,12 +142,14 @@ function handleStreamingResponse(
|
|
|
142
142
|
const capture = new RawStreamCapture();
|
|
143
143
|
const decoder = new TextDecoder();
|
|
144
144
|
let finalizationScheduled = false;
|
|
145
|
+
let firstChunkMs: number | null = null;
|
|
145
146
|
|
|
146
147
|
const scheduleStreamingFinalization = (type: "streaming" | "stream-abort"): void => {
|
|
147
148
|
if (finalizationScheduled) return;
|
|
148
149
|
finalizationScheduled = true;
|
|
149
150
|
|
|
150
|
-
const
|
|
151
|
+
const totalStreamMs = Date.now() - startTime;
|
|
152
|
+
const elapsedMs = totalStreamMs;
|
|
151
153
|
const rawStream = capture.snapshot();
|
|
152
154
|
switch (type) {
|
|
153
155
|
case "streaming":
|
|
@@ -159,6 +161,8 @@ function handleStreamingResponse(
|
|
|
159
161
|
responseStatus: upstreamRes.status,
|
|
160
162
|
rawStream,
|
|
161
163
|
collectStreamingChunks,
|
|
164
|
+
firstChunkMs,
|
|
165
|
+
totalStreamMs,
|
|
162
166
|
});
|
|
163
167
|
return;
|
|
164
168
|
case "stream-abort":
|
|
@@ -170,6 +174,8 @@ function handleStreamingResponse(
|
|
|
170
174
|
rawStream,
|
|
171
175
|
hasChunks: capture.hasChunks,
|
|
172
176
|
collectStreamingChunks,
|
|
177
|
+
firstChunkMs,
|
|
178
|
+
totalStreamMs,
|
|
173
179
|
});
|
|
174
180
|
return;
|
|
175
181
|
}
|
|
@@ -177,6 +183,7 @@ function handleStreamingResponse(
|
|
|
177
183
|
|
|
178
184
|
const transform = new TransformStream<Uint8Array, Uint8Array>({
|
|
179
185
|
async transform(chunk, controller) {
|
|
186
|
+
if (firstChunkMs === null) firstChunkMs = Date.now() - startTime;
|
|
180
187
|
controller.enqueue(chunk);
|
|
181
188
|
await capture.append(decoder.decode(chunk, { stream: true }));
|
|
182
189
|
},
|
|
@@ -8,6 +8,7 @@ import type { RawStreamSource } from "./rawStreamCapture";
|
|
|
8
8
|
import type { CapturedLog } from "./schemas";
|
|
9
9
|
import { getSessionProcess, isSessionProcessAvailable } from "./sessionProcess";
|
|
10
10
|
import { finalizeLogUpdate } from "./store";
|
|
11
|
+
import { computeTokensPerSecond } from "./streamTiming";
|
|
11
12
|
|
|
12
13
|
type BaseFinalizeLogJob = {
|
|
13
14
|
log: CapturedLog;
|
|
@@ -26,6 +27,8 @@ export type FinalizeStreamingLogJob = BaseFinalizeLogJob & {
|
|
|
26
27
|
responseStatus: number;
|
|
27
28
|
rawStream: string | RawStreamSource;
|
|
28
29
|
collectStreamingChunks: boolean;
|
|
30
|
+
firstChunkMs: number | null;
|
|
31
|
+
totalStreamMs: number;
|
|
29
32
|
};
|
|
30
33
|
|
|
31
34
|
export type FinalizeStreamAbortLogJob = BaseFinalizeLogJob & {
|
|
@@ -33,6 +36,8 @@ export type FinalizeStreamAbortLogJob = BaseFinalizeLogJob & {
|
|
|
33
36
|
rawStream: string | RawStreamSource;
|
|
34
37
|
hasChunks: boolean;
|
|
35
38
|
collectStreamingChunks: boolean;
|
|
39
|
+
firstChunkMs: number | null;
|
|
40
|
+
totalStreamMs: number;
|
|
36
41
|
};
|
|
37
42
|
|
|
38
43
|
export type FinalizeLogJob =
|
|
@@ -73,6 +78,9 @@ export function buildFileLogEntry(log: CapturedLog, upstreamUrl: string): Record
|
|
|
73
78
|
cacheCreationInputTokens: log.cacheCreationInputTokens,
|
|
74
79
|
cacheReadInputTokens: log.cacheReadInputTokens,
|
|
75
80
|
elapsedMs: log.elapsedMs,
|
|
81
|
+
firstChunkMs: log.firstChunkMs,
|
|
82
|
+
totalStreamMs: log.totalStreamMs,
|
|
83
|
+
tokensPerSecond: log.tokensPerSecond,
|
|
76
84
|
streaming: log.streaming,
|
|
77
85
|
userAgent: log.userAgent,
|
|
78
86
|
origin: log.origin,
|
|
@@ -119,10 +127,31 @@ function finalizeWithError(
|
|
|
119
127
|
log.responseStatus = log.responseStatus ?? fallbackStatus;
|
|
120
128
|
log.responseText = log.responseText ?? fallbackResponseText;
|
|
121
129
|
log.elapsedMs = job.elapsedMs;
|
|
130
|
+
applyOptionalStreamingTiming(log, job);
|
|
122
131
|
log.error = message;
|
|
123
132
|
return { log, upstreamUrl: job.upstreamUrl, error: message, cleanupRawStreamPath };
|
|
124
133
|
}
|
|
125
134
|
|
|
135
|
+
function applyOptionalStreamingTiming(log: CapturedLog, job: FinalizeLogJob): void {
|
|
136
|
+
switch (job.type) {
|
|
137
|
+
case "non-streaming":
|
|
138
|
+
log.firstChunkMs = null;
|
|
139
|
+
log.totalStreamMs = null;
|
|
140
|
+
log.tokensPerSecond = null;
|
|
141
|
+
return;
|
|
142
|
+
case "streaming":
|
|
143
|
+
case "stream-abort":
|
|
144
|
+
log.firstChunkMs = job.firstChunkMs;
|
|
145
|
+
log.totalStreamMs = job.totalStreamMs;
|
|
146
|
+
log.tokensPerSecond = computeTokensPerSecond({
|
|
147
|
+
outputTokens: log.outputTokens,
|
|
148
|
+
firstChunkMs: job.firstChunkMs,
|
|
149
|
+
totalStreamMs: job.totalStreamMs,
|
|
150
|
+
});
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
126
155
|
function finalizeNonStreaming(
|
|
127
156
|
job: FinalizeNonStreamingLogJob,
|
|
128
157
|
log: CapturedLog,
|
|
@@ -136,6 +165,9 @@ function finalizeNonStreaming(
|
|
|
136
165
|
const tokens = formatHandler.extractTokens(job.responseBody);
|
|
137
166
|
|
|
138
167
|
log.elapsedMs = job.elapsedMs;
|
|
168
|
+
log.firstChunkMs = null;
|
|
169
|
+
log.totalStreamMs = null;
|
|
170
|
+
log.tokensPerSecond = null;
|
|
139
171
|
log.responseStatus = job.responseStatus;
|
|
140
172
|
log.responseText = job.responseBody;
|
|
141
173
|
log.inputTokens = tokens.inputTokens;
|
|
@@ -238,6 +270,7 @@ function finalizeStreaming(job: FinalizeStreamingLogJob, log: CapturedLog): Fina
|
|
|
238
270
|
const oversized = getOversizedRawStream(job.rawStream);
|
|
239
271
|
if (oversized !== null) {
|
|
240
272
|
log.elapsedMs = job.elapsedMs;
|
|
273
|
+
applyOptionalStreamingTiming(log, job);
|
|
241
274
|
log.responseStatus = job.responseStatus;
|
|
242
275
|
log.responseText = oversizedRawStreamText(oversized);
|
|
243
276
|
return {
|
|
@@ -262,6 +295,7 @@ function finalizeStreaming(job: FinalizeStreamingLogJob, log: CapturedLog): Fina
|
|
|
262
295
|
log.model ?? undefined,
|
|
263
296
|
job.collectStreamingChunks,
|
|
264
297
|
);
|
|
298
|
+
applyOptionalStreamingTiming(log, job);
|
|
265
299
|
persistStreamingChunks(log);
|
|
266
300
|
|
|
267
301
|
return { log, upstreamUrl: job.upstreamUrl, error: null, cleanupRawStreamPath };
|
|
@@ -294,6 +328,7 @@ function finalizeStreamAbort(job: FinalizeStreamAbortLogJob, log: CapturedLog):
|
|
|
294
328
|
let cleanupRawStreamPath: string | null = null;
|
|
295
329
|
try {
|
|
296
330
|
log.elapsedMs = job.elapsedMs;
|
|
331
|
+
applyOptionalStreamingTiming(log, job);
|
|
297
332
|
if (job.hasChunks && formatHandler !== null) {
|
|
298
333
|
const oversized = getOversizedRawStream(job.rawStream);
|
|
299
334
|
if (oversized !== null) {
|
|
@@ -314,6 +349,7 @@ function finalizeStreamAbort(job: FinalizeStreamAbortLogJob, log: CapturedLog):
|
|
|
314
349
|
log.model ?? undefined,
|
|
315
350
|
job.collectStreamingChunks,
|
|
316
351
|
);
|
|
352
|
+
applyOptionalStreamingTiming(log, job);
|
|
317
353
|
persistStreamingChunks(log);
|
|
318
354
|
} else {
|
|
319
355
|
log.responseText = "Client aborted";
|
package/src/proxy/schemas.ts
CHANGED
|
@@ -40,6 +40,9 @@ export const CapturedLogSchema = z.object({
|
|
|
40
40
|
cacheCreationInputTokens: z.number().nullable(),
|
|
41
41
|
cacheReadInputTokens: z.number().nullable(),
|
|
42
42
|
elapsedMs: z.number().nullable(),
|
|
43
|
+
firstChunkMs: z.number().nullable().optional(),
|
|
44
|
+
totalStreamMs: z.number().nullable().optional(),
|
|
45
|
+
tokensPerSecond: z.number().nullable().optional(),
|
|
43
46
|
streaming: z.boolean(),
|
|
44
47
|
userAgent: z.string().nullable(),
|
|
45
48
|
origin: z.string().nullable(),
|
package/src/proxy/sessionInfo.ts
CHANGED
|
@@ -55,6 +55,9 @@ export function buildSessionLogSummary(log: CapturedLog): SessionLogSummary {
|
|
|
55
55
|
hasError: hasLogError(log),
|
|
56
56
|
error: truncateError(log.error),
|
|
57
57
|
latencyMs: log.elapsedMs,
|
|
58
|
+
firstChunkMs: log.firstChunkMs ?? null,
|
|
59
|
+
totalStreamMs: log.totalStreamMs ?? null,
|
|
60
|
+
tokensPerSecond: log.tokensPerSecond ?? null,
|
|
58
61
|
tokens: {
|
|
59
62
|
input: log.inputTokens,
|
|
60
63
|
output: log.outputTokens,
|
package/src/proxy/store.ts
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { StreamingChunk } from "./schemas";
|
|
2
|
+
|
|
3
|
+
export function computeTokensPerSecond({
|
|
4
|
+
outputTokens,
|
|
5
|
+
firstChunkMs,
|
|
6
|
+
totalStreamMs,
|
|
7
|
+
}: {
|
|
8
|
+
outputTokens: number | null | undefined;
|
|
9
|
+
firstChunkMs: number | null | undefined;
|
|
10
|
+
totalStreamMs: number | null | undefined;
|
|
11
|
+
}): number | null {
|
|
12
|
+
if (outputTokens === null || outputTokens === undefined || outputTokens <= 0) return null;
|
|
13
|
+
if (totalStreamMs === null || totalStreamMs === undefined || totalStreamMs <= 0) return null;
|
|
14
|
+
|
|
15
|
+
const generationMs =
|
|
16
|
+
firstChunkMs === null || firstChunkMs === undefined
|
|
17
|
+
? totalStreamMs
|
|
18
|
+
: totalStreamMs - firstChunkMs;
|
|
19
|
+
if (!Number.isFinite(generationMs) || generationMs <= 0) return null;
|
|
20
|
+
|
|
21
|
+
return Number((outputTokens / (generationMs / 1000)).toFixed(2));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function firstChunkMsFromChunks(
|
|
25
|
+
chunks: readonly StreamingChunk[] | undefined,
|
|
26
|
+
): number | null {
|
|
27
|
+
const first = chunks?.[0];
|
|
28
|
+
if (first === undefined) return null;
|
|
29
|
+
if (!Number.isFinite(first.timestamp) || first.timestamp < 0) return null;
|
|
30
|
+
return Math.floor(first.timestamp);
|
|
31
|
+
}
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
selectUpstreamBase,
|
|
22
22
|
setUpstreamHost,
|
|
23
23
|
} from "../../proxy/upstream";
|
|
24
|
+
import { computeTokensPerSecond } from "../../proxy/streamTiming";
|
|
24
25
|
|
|
25
26
|
type ReplayRequest = {
|
|
26
27
|
modifiedBody: string;
|
|
@@ -38,6 +39,9 @@ type ReplayResponse = {
|
|
|
38
39
|
inputTokens?: number;
|
|
39
40
|
outputTokens?: number;
|
|
40
41
|
elapsedMs?: number;
|
|
42
|
+
firstChunkMs?: number | null;
|
|
43
|
+
totalStreamMs?: number | null;
|
|
44
|
+
tokensPerSecond?: number | null;
|
|
41
45
|
streaming?: boolean;
|
|
42
46
|
replayLogId?: number;
|
|
43
47
|
};
|
|
@@ -135,7 +139,6 @@ export const Route = createFileRoute("/api/logs/$id/replay")({
|
|
|
135
139
|
} satisfies ReplayResponse);
|
|
136
140
|
}
|
|
137
141
|
|
|
138
|
-
const elapsedMs = Date.now() - startTime;
|
|
139
142
|
const isStream =
|
|
140
143
|
upstreamRes.headers.get(HEADER_CONTENT_TYPE)?.includes(CONTENT_TYPE_EVENT_STREAM) ??
|
|
141
144
|
false;
|
|
@@ -145,11 +148,13 @@ export const Route = createFileRoute("/api/logs/$id/replay")({
|
|
|
145
148
|
const chunks: string[] = [];
|
|
146
149
|
const decoder = new TextDecoder();
|
|
147
150
|
const reader = upstreamRes.body?.getReader();
|
|
151
|
+
let firstChunkMs: number | null = null;
|
|
148
152
|
if (reader) {
|
|
149
153
|
try {
|
|
150
154
|
while (true) {
|
|
151
155
|
const { done, value } = await reader.read();
|
|
152
156
|
if (done) break;
|
|
157
|
+
if (firstChunkMs === null) firstChunkMs = Date.now() - startTime;
|
|
153
158
|
chunks.push(decoder.decode(value, { stream: true }));
|
|
154
159
|
}
|
|
155
160
|
} catch {
|
|
@@ -157,11 +162,19 @@ export const Route = createFileRoute("/api/logs/$id/replay")({
|
|
|
157
162
|
}
|
|
158
163
|
}
|
|
159
164
|
const fullResponse = chunks.join("");
|
|
165
|
+
const elapsedMs = Date.now() - startTime;
|
|
166
|
+
const totalStreamMs = elapsedMs;
|
|
160
167
|
|
|
161
168
|
// Create a mock log for streaming extraction
|
|
162
169
|
const mockLog: typeof log = { ...log };
|
|
163
170
|
const responseText = formatHandler.extractStream(fullResponse, mockLog, model, false);
|
|
164
171
|
const tokens = formatHandler.extractTokens(responseText);
|
|
172
|
+
const outputTokens = tokens.outputTokens ?? null;
|
|
173
|
+
const tokensPerSecond = computeTokensPerSecond({
|
|
174
|
+
outputTokens,
|
|
175
|
+
firstChunkMs,
|
|
176
|
+
totalStreamMs,
|
|
177
|
+
});
|
|
165
178
|
const savedReplayLog = await addTestLogEntry({
|
|
166
179
|
timestamp: new Date().toISOString(),
|
|
167
180
|
method: log.method,
|
|
@@ -172,10 +185,13 @@ export const Route = createFileRoute("/api/logs/$id/replay")({
|
|
|
172
185
|
responseStatus: upstreamRes.status,
|
|
173
186
|
responseText,
|
|
174
187
|
inputTokens: tokens.inputTokens ?? null,
|
|
175
|
-
outputTokens
|
|
188
|
+
outputTokens,
|
|
176
189
|
cacheCreationInputTokens: tokens.cacheCreationInputTokens ?? null,
|
|
177
190
|
cacheReadInputTokens: tokens.cacheReadInputTokens ?? null,
|
|
178
191
|
elapsedMs,
|
|
192
|
+
firstChunkMs,
|
|
193
|
+
totalStreamMs,
|
|
194
|
+
tokensPerSecond,
|
|
179
195
|
streaming: true,
|
|
180
196
|
userAgent: log.userAgent,
|
|
181
197
|
origin: log.origin,
|
|
@@ -201,11 +217,15 @@ export const Route = createFileRoute("/api/logs/$id/replay")({
|
|
|
201
217
|
inputTokens: tokens.inputTokens ?? undefined,
|
|
202
218
|
outputTokens: tokens.outputTokens ?? undefined,
|
|
203
219
|
elapsedMs,
|
|
220
|
+
firstChunkMs,
|
|
221
|
+
totalStreamMs,
|
|
222
|
+
tokensPerSecond,
|
|
204
223
|
streaming: true,
|
|
205
224
|
replayLogId: savedReplayLog.id,
|
|
206
225
|
} satisfies ReplayResponse);
|
|
207
226
|
} else {
|
|
208
227
|
const responseText = await upstreamRes.text();
|
|
228
|
+
const elapsedMs = Date.now() - startTime;
|
|
209
229
|
const tokens = formatHandler.extractTokens(responseText);
|
|
210
230
|
const savedReplayLog = await addTestLogEntry({
|
|
211
231
|
timestamp: new Date().toISOString(),
|
|
@@ -221,6 +241,9 @@ export const Route = createFileRoute("/api/logs/$id/replay")({
|
|
|
221
241
|
cacheCreationInputTokens: tokens.cacheCreationInputTokens ?? null,
|
|
222
242
|
cacheReadInputTokens: tokens.cacheReadInputTokens ?? null,
|
|
223
243
|
elapsedMs,
|
|
244
|
+
firstChunkMs: null,
|
|
245
|
+
totalStreamMs: null,
|
|
246
|
+
tokensPerSecond: null,
|
|
224
247
|
streaming: false,
|
|
225
248
|
userAgent: log.userAgent,
|
|
226
249
|
origin: log.origin,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type ProviderTestResult as TestResult,
|
|
9
9
|
type ProviderTestState,
|
|
10
10
|
} from "../../lib/providerTestContract";
|
|
11
|
+
import { computeTokensPerSecond, firstChunkMsFromChunks } from "../../proxy/streamTiming";
|
|
11
12
|
|
|
12
13
|
function hasSuccessField(result: ProviderTestState): result is TestResult {
|
|
13
14
|
return Object.prototype.hasOwnProperty.call(result, "success");
|
|
@@ -29,6 +30,25 @@ function getResultHeaders(result: TestResult): Record<string, string> {
|
|
|
29
30
|
return result.requestHeaders ?? result.debug?.requestHeaders ?? {};
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
function getResultTotalStreamMs(result: TestResult): number | null {
|
|
34
|
+
return result.totalStreamMs ?? (result.streaming === true ? (result.latencyMs ?? null) : null);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getResultFirstChunkMs(result: TestResult): number | null {
|
|
38
|
+
return result.firstChunkMs ?? firstChunkMsFromChunks(result.streamingChunks?.chunks);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getResultTokensPerSecond(result: TestResult): number | null {
|
|
42
|
+
return (
|
|
43
|
+
result.tokensPerSecond ??
|
|
44
|
+
computeTokensPerSecond({
|
|
45
|
+
outputTokens: result.outputTokens,
|
|
46
|
+
firstChunkMs: getResultFirstChunkMs(result),
|
|
47
|
+
totalStreamMs: getResultTotalStreamMs(result),
|
|
48
|
+
})
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
32
52
|
async function logModelResults(
|
|
33
53
|
displayName: string,
|
|
34
54
|
providerName: string,
|
|
@@ -65,6 +85,9 @@ async function logModelResults(
|
|
|
65
85
|
cacheCreationInputTokens: nonStreamingResult.cacheCreationInputTokens ?? null,
|
|
66
86
|
cacheReadInputTokens: nonStreamingResult.cacheReadInputTokens ?? null,
|
|
67
87
|
elapsedMs: nonStreamingResult.latencyMs ?? 0,
|
|
88
|
+
firstChunkMs: null,
|
|
89
|
+
totalStreamMs: null,
|
|
90
|
+
tokensPerSecond: null,
|
|
68
91
|
streaming: false,
|
|
69
92
|
userAgent: "provider-test",
|
|
70
93
|
origin: null,
|
|
@@ -91,7 +114,10 @@ async function logModelResults(
|
|
|
91
114
|
outputTokens: streamingResult.outputTokens ?? null,
|
|
92
115
|
cacheCreationInputTokens: streamingResult.cacheCreationInputTokens ?? null,
|
|
93
116
|
cacheReadInputTokens: streamingResult.cacheReadInputTokens ?? null,
|
|
94
|
-
elapsedMs: streamingResult
|
|
117
|
+
elapsedMs: getResultTotalStreamMs(streamingResult) ?? 0,
|
|
118
|
+
firstChunkMs: getResultFirstChunkMs(streamingResult),
|
|
119
|
+
totalStreamMs: getResultTotalStreamMs(streamingResult),
|
|
120
|
+
tokensPerSecond: getResultTokensPerSecond(streamingResult),
|
|
95
121
|
streaming: true,
|
|
96
122
|
streamingChunks: captureFullDetails ? streamingResult.streamingChunks : undefined,
|
|
97
123
|
userAgent: "provider-test",
|
|
@@ -127,6 +153,9 @@ async function logModelResults(
|
|
|
127
153
|
cacheCreationInputTokens: null,
|
|
128
154
|
cacheReadInputTokens: null,
|
|
129
155
|
elapsedMs: nonStreamingResult.latencyMs ?? 0,
|
|
156
|
+
firstChunkMs: null,
|
|
157
|
+
totalStreamMs: null,
|
|
158
|
+
tokensPerSecond: null,
|
|
130
159
|
streaming: false,
|
|
131
160
|
userAgent: "provider-test",
|
|
132
161
|
origin: null,
|
|
@@ -153,7 +182,10 @@ async function logModelResults(
|
|
|
153
182
|
outputTokens: streamingResult.outputTokens ?? null,
|
|
154
183
|
cacheCreationInputTokens: null,
|
|
155
184
|
cacheReadInputTokens: null,
|
|
156
|
-
elapsedMs: streamingResult
|
|
185
|
+
elapsedMs: getResultTotalStreamMs(streamingResult) ?? 0,
|
|
186
|
+
firstChunkMs: getResultFirstChunkMs(streamingResult),
|
|
187
|
+
totalStreamMs: getResultTotalStreamMs(streamingResult),
|
|
188
|
+
tokensPerSecond: getResultTokensPerSecond(streamingResult),
|
|
157
189
|
streaming: true,
|
|
158
190
|
streamingChunks: captureFullDetails ? streamingResult.streamingChunks : undefined,
|
|
159
191
|
userAgent: "provider-test",
|