pi-firecode 0.8.0 → 1.0.0
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 +35 -15
- package/activity.ts +92 -0
- package/busy.ts +188 -0
- package/config.example.jsonc +38 -47
- package/config.ts +198 -172
- package/deliver.ts +100 -13
- package/flame.ts +149 -0
- package/format.ts +84 -31
- package/header.ts +208 -82
- package/index.ts +17 -11
- package/master/actions.ts +293 -0
- package/master/activity-list.ts +281 -0
- package/master/event-card.ts +18 -37
- package/master/event-format.ts +71 -40
- package/master/guard.ts +46 -0
- package/master/index.ts +88 -1036
- package/master/list-view.ts +129 -0
- package/master/outbox.ts +188 -0
- package/master/prompts/master.zh.md +18 -13
- package/master/prompts/worker.zh.md +1 -1
- package/master/role.ts +6 -1
- package/master/run.ts +315 -0
- package/master/runtime.ts +259 -0
- package/master/spawn.ts +79 -38
- package/master/state.ts +30 -6
- package/master/worker-view.ts +517 -0
- package/package.json +1 -1
- package/provider/claude-sub.ts +35 -13
- package/provider/openai-native/src/compact-client.ts +2 -14
- package/provider/openai-native/src/options.ts +4 -2
- package/review/advisor.ts +0 -2
- package/review/card.ts +36 -23
- package/review/checkpoint.ts +2 -8
- package/review/evidence.ts +42 -44
- package/review/index.ts +271 -525
- package/review/occupancy.ts +25 -0
- package/review/outcome.ts +31 -7
- package/review/prompt.ts +7 -8
- package/review/prompts/review.en.md +7 -6
- package/review/prompts/review.zh.md +7 -6
- package/review/reviewer.ts +2 -5
- package/review/session.ts +11 -30
- package/review/state.ts +32 -60
- package/review/ui.ts +30 -469
- package/round-recorder.ts +18 -0
- package/session/herdr-display.ts +1 -1
- package/session/presets.ts +130 -87
- package/session/quota.ts +123 -0
- package/session/stats.ts +4 -1
- package/statusbar/index.ts +242 -104
- package/statusbar/render.ts +95 -163
- package/theme.ts +2 -41
- package/tools/actions.ts +53 -0
- package/tools/assistant-view.ts +64 -0
- package/tools/click-anchor.ts +56 -0
- package/tools/group-view.ts +420 -0
- package/tools/grouping.ts +146 -221
- package/tools/host.ts +369 -0
- package/tools/index.ts +108 -105
- package/tools/line.ts +95 -58
- package/tools/machine.ts +78 -0
- package/tools/parts.ts +2 -2
- package/tools/round.ts +95 -0
- package/tools/turn-clock.ts +41 -0
- package/tools/turn-summary.ts +109 -0
- package/truncated-write.ts +18 -0
- package/watcher/card.ts +28 -22
- package/watcher/index.ts +16 -44
- package/watcher/observer.ts +3 -3
- package/watcher/prompts/watch.zh.md +1 -1
- package/watcher/transcript.ts +4 -12
- package/flame-frames.ts +0 -460
- package/review/progress.ts +0 -324
- package/session/bark.ts +0 -157
- package/session/working-flame.ts +0 -116
- package/statusbar/quota-cache.ts +0 -60
- package/statusbar/quota-parse.ts +0 -85
- package/statusbar/quota.ts +0 -203
- package/statusbar/tps.ts +0 -87
package/statusbar/quota-parse.ts
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
/** 供应商用量响应 → 剩余百分比窗口。响应结构不是公开契约,解析一律容错。 */
|
|
2
|
-
|
|
3
|
-
type JsonRecord = Record<string, unknown>;
|
|
4
|
-
|
|
5
|
-
export type QuotaWindow = {
|
|
6
|
-
label: string;
|
|
7
|
-
remaining: number;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
export type QuotaStatus =
|
|
11
|
-
| { state: "loading" }
|
|
12
|
-
| { state: "ready"; windows: QuotaWindow[] }
|
|
13
|
-
| { state: "unavailable" };
|
|
14
|
-
|
|
15
|
-
export const record = (value: unknown): JsonRecord | undefined =>
|
|
16
|
-
typeof value === "object" && value !== null ? (value as JsonRecord) : undefined;
|
|
17
|
-
|
|
18
|
-
export const finiteNumber = (value: unknown): number | undefined =>
|
|
19
|
-
typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
20
|
-
|
|
21
|
-
/** 秒级时间戳或 ISO 字符串 → 毫秒时间戳。 */
|
|
22
|
-
export const epochMillis = (value: unknown): number | undefined => {
|
|
23
|
-
if (typeof value === "number" && Number.isFinite(value)) return value * 1_000;
|
|
24
|
-
if (typeof value !== "string") return undefined;
|
|
25
|
-
const parsed = Date.parse(value);
|
|
26
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const remainingPercent = (used: number) =>
|
|
30
|
-
Math.round(Math.max(0, Math.min(100, 100 - used)));
|
|
31
|
-
|
|
32
|
-
function windowFromUsage(
|
|
33
|
-
label: string,
|
|
34
|
-
value: unknown,
|
|
35
|
-
usedKey: string,
|
|
36
|
-
): QuotaWindow | undefined {
|
|
37
|
-
const used = finiteNumber(record(value)?.[usedKey]);
|
|
38
|
-
return used === undefined ? undefined : { label, remaining: remainingPercent(used) };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const defined = (window: QuotaWindow | undefined): window is QuotaWindow =>
|
|
42
|
-
Boolean(window);
|
|
43
|
-
|
|
44
|
-
export function parseOpenAIQuota(value: unknown): QuotaWindow[] {
|
|
45
|
-
const rateLimit = record(record(value)?.rate_limit);
|
|
46
|
-
const primary = record(rateLimit?.primary_window);
|
|
47
|
-
const secondary = record(rateLimit?.secondary_window);
|
|
48
|
-
const label = (window: JsonRecord | undefined, fallback: string) => {
|
|
49
|
-
const seconds = finiteNumber(window?.limit_window_seconds);
|
|
50
|
-
if (!seconds) return fallback;
|
|
51
|
-
if (seconds % 86_400 === 0) return `${seconds / 86_400}d`;
|
|
52
|
-
if (seconds % 3_600 === 0) return `${seconds / 3_600}h`;
|
|
53
|
-
return fallback;
|
|
54
|
-
};
|
|
55
|
-
return [
|
|
56
|
-
windowFromUsage(label(primary, "5h"), primary, "used_percent"),
|
|
57
|
-
windowFromUsage(label(secondary, "7d"), secondary, "used_percent"),
|
|
58
|
-
].filter(defined);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function parseAnthropicQuota(value: unknown): QuotaWindow[] {
|
|
62
|
-
const source = record(value);
|
|
63
|
-
return [
|
|
64
|
-
windowFromUsage("5h", source?.five_hour, "utilization"),
|
|
65
|
-
windowFromUsage("7d", source?.seven_day, "utilization"),
|
|
66
|
-
].filter(defined);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function parseGrokQuota(
|
|
70
|
-
monthlyValue: unknown,
|
|
71
|
-
weeklyValue: unknown,
|
|
72
|
-
): QuotaWindow[] {
|
|
73
|
-
const monthly = record(record(monthlyValue)?.config);
|
|
74
|
-
const weekly = record(record(weeklyValue)?.config);
|
|
75
|
-
const monthlyLimit = finiteNumber(record(monthly?.monthlyLimit)?.val);
|
|
76
|
-
const monthlyUsed = finiteNumber(record(monthly?.used)?.val);
|
|
77
|
-
const monthlyWindow =
|
|
78
|
-
monthlyLimit !== undefined && monthlyLimit > 0 && monthlyUsed !== undefined
|
|
79
|
-
? { label: "30d", remaining: remainingPercent((monthlyUsed / monthlyLimit) * 100) }
|
|
80
|
-
: undefined;
|
|
81
|
-
return [
|
|
82
|
-
windowFromUsage("7d", weekly, "creditUsagePercent"),
|
|
83
|
-
monthlyWindow,
|
|
84
|
-
].filter(defined);
|
|
85
|
-
}
|
package/statusbar/quota.ts
DELETED
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 订阅额度抓取:事件驱动(会话启动、切模型、每轮结束),无定时轮询。
|
|
3
|
-
* 新鲜度与失败退避都落在跨进程缓存里,多个 pi 会话共享同一次请求。
|
|
4
|
-
*/
|
|
5
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
-
import { homedir } from "node:os";
|
|
7
|
-
import { join } from "node:path";
|
|
8
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import type { QuotaCache } from "./quota-cache.js";
|
|
10
|
-
import {
|
|
11
|
-
type QuotaStatus,
|
|
12
|
-
type QuotaWindow,
|
|
13
|
-
epochMillis,
|
|
14
|
-
parseAnthropicQuota,
|
|
15
|
-
parseGrokQuota,
|
|
16
|
-
parseOpenAIQuota,
|
|
17
|
-
record,
|
|
18
|
-
} from "./quota-parse.js";
|
|
19
|
-
|
|
20
|
-
// 供应商自家 CLI 使用的只读接口,schema 不是公开契约。
|
|
21
|
-
const OPENAI_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
22
|
-
const ANTHROPIC_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
23
|
-
const GROK_BILLING_URL = "https://cli-chat-proxy.grok.com/v1/billing";
|
|
24
|
-
const FRESH_MS = 60_000;
|
|
25
|
-
const REQUEST_TIMEOUT_MS = 3_000;
|
|
26
|
-
/** 连续失败的退避档位,末档为上限 */
|
|
27
|
-
const BACKOFF_MS = [60_000, 120_000, 300_000];
|
|
28
|
-
|
|
29
|
-
function decodeJwtPayload(token: string): ReturnType<typeof record> {
|
|
30
|
-
const payload = token.split(".")[1];
|
|
31
|
-
if (!payload) return undefined;
|
|
32
|
-
try {
|
|
33
|
-
return record(JSON.parse(Buffer.from(payload, "base64url").toString("utf8")));
|
|
34
|
-
} catch {
|
|
35
|
-
return undefined;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function openAIAccountId(token: string): string | undefined {
|
|
40
|
-
const claims = record(decodeJwtPayload(token)?.["https://api.openai.com/auth"]);
|
|
41
|
-
return typeof claims?.chatgpt_account_id === "string" ? claims.chatgpt_account_id : undefined;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function grokCliToken(): string | undefined {
|
|
45
|
-
// Grok billing 拒绝 pi 的 OAuth token,只认官方 CLI 登录态。
|
|
46
|
-
const path = join(homedir(), ".grok", "auth.json");
|
|
47
|
-
if (!existsSync(path)) return undefined;
|
|
48
|
-
try {
|
|
49
|
-
const auth = record(JSON.parse(readFileSync(path, "utf8")));
|
|
50
|
-
for (const [scope, value] of Object.entries(auth ?? {})) {
|
|
51
|
-
if (!scope.startsWith("https://auth.x.ai::")) continue;
|
|
52
|
-
const credential = record(value);
|
|
53
|
-
const token = credential?.key;
|
|
54
|
-
const expiresAt = epochMillis(credential?.expires_at);
|
|
55
|
-
if (typeof token === "string" && token.length > 0 && (!expiresAt || expiresAt > Date.now()))
|
|
56
|
-
return token;
|
|
57
|
-
}
|
|
58
|
-
} catch {
|
|
59
|
-
return undefined;
|
|
60
|
-
}
|
|
61
|
-
return undefined;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async function fetchJson(url: string, headers: Record<string, string>): Promise<unknown> {
|
|
65
|
-
const response = await fetch(url, {
|
|
66
|
-
headers,
|
|
67
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
68
|
-
});
|
|
69
|
-
if (!response.ok) throw new Error(`Quota request failed: ${response.status}`);
|
|
70
|
-
return response.json();
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
type QuotaRequest =
|
|
74
|
-
| { provider: "openai-codex" | "anthropic"; token: Promise<string | undefined> }
|
|
75
|
-
| { provider: "xai"; token: string | undefined };
|
|
76
|
-
|
|
77
|
-
type QuotaProvider = QuotaRequest["provider"];
|
|
78
|
-
|
|
79
|
-
async function loadQuota(request: QuotaRequest): Promise<QuotaWindow[]> {
|
|
80
|
-
if (request.provider === "openai-codex") {
|
|
81
|
-
const token = await request.token;
|
|
82
|
-
if (!token) return [];
|
|
83
|
-
const accountId = openAIAccountId(token);
|
|
84
|
-
return parseOpenAIQuota(
|
|
85
|
-
await fetchJson(OPENAI_USAGE_URL, {
|
|
86
|
-
Authorization: `Bearer ${token}`,
|
|
87
|
-
...(accountId ? { "ChatGPT-Account-Id": accountId } : {}),
|
|
88
|
-
Accept: "application/json",
|
|
89
|
-
}),
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
if (request.provider === "anthropic") {
|
|
93
|
-
const token = await request.token;
|
|
94
|
-
if (!token) return [];
|
|
95
|
-
return parseAnthropicQuota(
|
|
96
|
-
await fetchJson(ANTHROPIC_USAGE_URL, {
|
|
97
|
-
Authorization: `Bearer ${token}`,
|
|
98
|
-
"anthropic-beta": "oauth-2025-04-20",
|
|
99
|
-
Accept: "application/json",
|
|
100
|
-
}),
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
const { token } = request;
|
|
104
|
-
if (!token) return [];
|
|
105
|
-
const headers = {
|
|
106
|
-
Authorization: `Bearer ${token}`,
|
|
107
|
-
"x-xai-token-auth": "xai-grok-cli",
|
|
108
|
-
Accept: "application/json",
|
|
109
|
-
};
|
|
110
|
-
const [monthly, weekly] = await Promise.allSettled([
|
|
111
|
-
fetchJson(GROK_BILLING_URL, headers),
|
|
112
|
-
fetchJson(`${GROK_BILLING_URL}?format=credits`, headers),
|
|
113
|
-
]);
|
|
114
|
-
if (monthly.status === "rejected" && weekly.status === "rejected") throw monthly.reason;
|
|
115
|
-
return parseGrokQuota(
|
|
116
|
-
monthly.status === "fulfilled" ? monthly.value : undefined,
|
|
117
|
-
weekly.status === "fulfilled" ? weekly.value : undefined,
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const isQuotaProvider = (provider: string | undefined): provider is QuotaProvider =>
|
|
122
|
-
provider === "openai-codex" || provider === "anthropic" || provider === "xai";
|
|
123
|
-
|
|
124
|
-
/** 只有订阅制(OAuth 登录)的供应商才有额度可查。 */
|
|
125
|
-
function subscriptionProvider(ctx: ExtensionContext): QuotaProvider | undefined {
|
|
126
|
-
const model = ctx.model;
|
|
127
|
-
const provider = model?.provider;
|
|
128
|
-
if (!isQuotaProvider(provider)) return undefined;
|
|
129
|
-
if (
|
|
130
|
-
(provider === "anthropic" || provider === "xai") &&
|
|
131
|
-
(!model || !ctx.modelRegistry.isUsingOAuth(model))
|
|
132
|
-
)
|
|
133
|
-
return undefined;
|
|
134
|
-
return provider;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function quotaRequest(ctx: ExtensionContext, provider: QuotaProvider): QuotaRequest {
|
|
138
|
-
if (provider === "xai") return { provider, token: grokCliToken() };
|
|
139
|
-
return { provider, token: ctx.modelRegistry.getApiKeyForProvider(provider) };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const statusOf = (windows: QuotaWindow[]): QuotaStatus =>
|
|
143
|
-
windows.length ? { state: "ready", windows } : { state: "unavailable" };
|
|
144
|
-
|
|
145
|
-
export function registerQuota(
|
|
146
|
-
pi: ExtensionAPI,
|
|
147
|
-
update: (status?: QuotaStatus) => void,
|
|
148
|
-
cache: QuotaCache,
|
|
149
|
-
) {
|
|
150
|
-
let requestGeneration = 0;
|
|
151
|
-
|
|
152
|
-
const refresh = (ctx: ExtensionContext, force = false) => {
|
|
153
|
-
const provider = subscriptionProvider(ctx);
|
|
154
|
-
if (!provider) {
|
|
155
|
-
requestGeneration++;
|
|
156
|
-
update();
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const now = Date.now();
|
|
161
|
-
const cached = cache.read(provider);
|
|
162
|
-
if (cached && now < cached.nextAttemptAt) {
|
|
163
|
-
requestGeneration++;
|
|
164
|
-
update(statusOf(cached.windows));
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
if (force) update({ state: "loading" });
|
|
168
|
-
|
|
169
|
-
const generation = ++requestGeneration;
|
|
170
|
-
// 先占住退避窗口,避免同一进程内并发请求叠加。
|
|
171
|
-
cache.write(provider, {
|
|
172
|
-
windows: cached?.windows ?? [],
|
|
173
|
-
nextAttemptAt: now + FRESH_MS,
|
|
174
|
-
failures: cached?.failures ?? 0,
|
|
175
|
-
});
|
|
176
|
-
void loadQuota(quotaRequest(ctx, provider))
|
|
177
|
-
.then((windows) => {
|
|
178
|
-
cache.write(provider, {
|
|
179
|
-
windows,
|
|
180
|
-
nextAttemptAt: Date.now() + FRESH_MS,
|
|
181
|
-
failures: 0,
|
|
182
|
-
});
|
|
183
|
-
if (generation === requestGeneration) update(statusOf(windows));
|
|
184
|
-
})
|
|
185
|
-
.catch(() => {
|
|
186
|
-
const failures = (cached?.failures ?? 0) + 1;
|
|
187
|
-
cache.write(provider, {
|
|
188
|
-
windows: [],
|
|
189
|
-
nextAttemptAt: Date.now() + BACKOFF_MS[Math.min(failures, BACKOFF_MS.length) - 1],
|
|
190
|
-
failures,
|
|
191
|
-
});
|
|
192
|
-
if (generation === requestGeneration) update({ state: "unavailable" });
|
|
193
|
-
});
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
pi.on("session_start", (_event, ctx) => refresh(ctx, true));
|
|
197
|
-
pi.on("model_select", (_event, ctx) => refresh(ctx, true));
|
|
198
|
-
pi.on("agent_end", (_event, ctx) => refresh(ctx));
|
|
199
|
-
pi.on("session_shutdown", () => {
|
|
200
|
-
requestGeneration++;
|
|
201
|
-
update();
|
|
202
|
-
});
|
|
203
|
-
}
|
package/statusbar/tps.ts
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/** Tracks one provider response from request start through final output usage. */
|
|
2
|
-
import { performance } from "node:perf_hooks";
|
|
3
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
|
|
5
|
-
const TOKEN_CHARS = 4;
|
|
6
|
-
const MIN_SAMPLE_MS = 1_000;
|
|
7
|
-
const UPDATE_INTERVAL_MS = 250;
|
|
8
|
-
|
|
9
|
-
export type TpsStatus =
|
|
10
|
-
| { phase: "live"; tokensPerSecond?: number }
|
|
11
|
-
| { phase: "complete"; elapsedSeconds: number; tokensPerSecond?: number };
|
|
12
|
-
|
|
13
|
-
function deltaText(event: { type: string; delta?: unknown }): string | undefined {
|
|
14
|
-
if (event.type !== "text_delta" && event.type !== "thinking_delta" && event.type !== "toolcall_delta") {
|
|
15
|
-
return undefined;
|
|
16
|
-
}
|
|
17
|
-
return typeof event.delta === "string" ? event.delta : undefined;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function registerTps(
|
|
21
|
-
pi: ExtensionAPI,
|
|
22
|
-
update: (status?: TpsStatus) => void,
|
|
23
|
-
now = (): number => performance.now(),
|
|
24
|
-
): void {
|
|
25
|
-
let requestStartedAt: number | undefined;
|
|
26
|
-
let firstDeltaAt: number | undefined;
|
|
27
|
-
let lastDeltaAt: number | undefined;
|
|
28
|
-
let lastUpdateAt: number | undefined;
|
|
29
|
-
let estimatedTokens = 0;
|
|
30
|
-
|
|
31
|
-
function resetMeasurement(): void {
|
|
32
|
-
requestStartedAt = undefined;
|
|
33
|
-
firstDeltaAt = undefined;
|
|
34
|
-
lastDeltaAt = undefined;
|
|
35
|
-
lastUpdateAt = undefined;
|
|
36
|
-
estimatedTokens = 0;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function clear(): void {
|
|
40
|
-
resetMeasurement();
|
|
41
|
-
update();
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
pi.on("session_start", clear);
|
|
45
|
-
pi.on("model_select", clear);
|
|
46
|
-
pi.on("before_provider_request", () => {
|
|
47
|
-
resetMeasurement();
|
|
48
|
-
requestStartedAt = now();
|
|
49
|
-
});
|
|
50
|
-
pi.on("message_update", (event) => {
|
|
51
|
-
if (event.message.role !== "assistant") return;
|
|
52
|
-
const delta = deltaText(event.assistantMessageEvent);
|
|
53
|
-
if (delta === undefined) return;
|
|
54
|
-
|
|
55
|
-
const current = now();
|
|
56
|
-
lastDeltaAt = current;
|
|
57
|
-
estimatedTokens += Math.max(0, delta.length / TOKEN_CHARS);
|
|
58
|
-
if (firstDeltaAt === undefined) {
|
|
59
|
-
firstDeltaAt = current;
|
|
60
|
-
lastUpdateAt = current;
|
|
61
|
-
update({ phase: "live" });
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const elapsed = current - firstDeltaAt;
|
|
66
|
-
if (elapsed < MIN_SAMPLE_MS || current - (lastUpdateAt ?? 0) < UPDATE_INTERVAL_MS) return;
|
|
67
|
-
lastUpdateAt = current;
|
|
68
|
-
const officialTokens = event.message.usage.output;
|
|
69
|
-
const tokens = officialTokens > 0 ? officialTokens : Math.round(estimatedTokens);
|
|
70
|
-
update({ phase: "live", tokensPerSecond: Math.round((tokens * 1_000) / elapsed) });
|
|
71
|
-
});
|
|
72
|
-
pi.on("message_end", (event) => {
|
|
73
|
-
if (event.message.role !== "assistant" || firstDeltaAt === undefined || lastDeltaAt === undefined) return;
|
|
74
|
-
const current = now();
|
|
75
|
-
const generationMs = lastDeltaAt - firstDeltaAt;
|
|
76
|
-
const outputTokens = event.message.usage.output;
|
|
77
|
-
update({
|
|
78
|
-
phase: "complete",
|
|
79
|
-
elapsedSeconds: Math.max(0, (current - (requestStartedAt ?? firstDeltaAt)) / 1_000),
|
|
80
|
-
...(generationMs > 0 && outputTokens > 0
|
|
81
|
-
? { tokensPerSecond: Math.round((outputTokens * 1_000) / generationMs) }
|
|
82
|
-
: {}),
|
|
83
|
-
});
|
|
84
|
-
resetMeasurement();
|
|
85
|
-
});
|
|
86
|
-
pi.on("session_shutdown", clear);
|
|
87
|
-
}
|