@linxiraos/pi-stats 1.2.0 → 1.3.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/CHANGELOG.md +15 -4
- package/README.md +4 -4
- package/THIRD-PARTY-NOTICES.txt +55 -5
- package/dist/client/index.js +63 -63
- package/dist/types/client/i18n/index.d.ts +1 -1
- package/dist/types/client/i18n/messages/en.d.ts +2 -2
- package/dist/types/client/i18n/messages/providers.d.ts +2 -2
- package/dist/types/index.d.ts +1 -1
- package/dist/types/parser.d.ts +11 -1
- package/dist/types/shared-types.d.ts +2 -2
- package/dist/types/usage-windows.d.ts +6 -4
- package/package.json +8 -8
- package/src/client/i18n/index.ts +1 -1
- package/src/client/i18n/messages/providers.ts +2 -2
- package/src/client/routes/FrustrationRoute.tsx +1 -1
- package/src/db.ts +22 -9
- package/src/frustration.ts +4 -3
- package/src/index.ts +1 -1
- package/src/live.ts +2 -1
- package/src/parser.ts +23 -2
- package/src/port-conflict.ts +2 -7
- package/src/shared-types.ts +2 -2
- package/src/trace.ts +8 -5
- package/src/usage-windows.ts +27 -6
|
@@ -5,7 +5,7 @@ export declare function getLocale(): StatsLocale;
|
|
|
5
5
|
/** Install a locale by tag (`zh`, `zh-CN`, `zh-Hans`; anything else is English). */
|
|
6
6
|
export declare function setLocale(tag: string | undefined | null): StatsLocale;
|
|
7
7
|
/**
|
|
8
|
-
* The locale to boot with: the host's language when `
|
|
8
|
+
* The locale to boot with: the host's language when `zetacode stats` injected it,
|
|
9
9
|
* then the browser's own preference, then English.
|
|
10
10
|
*/
|
|
11
11
|
export declare function resolveInitialLocale(): StatsLocale;
|
|
@@ -217,7 +217,7 @@ export declare const enMessages: {
|
|
|
217
217
|
readonly "providers.exhaustions-title": "Transitions into an exhausted state observed in range";
|
|
218
218
|
readonly "providers.exhausted-n": "{count} exhausted";
|
|
219
219
|
readonly "providers.no-snapshots-in-range": "No usage snapshots in this range";
|
|
220
|
-
readonly "providers.snapshot-hint": "Usage snapshots accumulate whenever usage limits are fetched (TUI footer, /usage,
|
|
220
|
+
readonly "providers.snapshot-hint": "Usage snapshots accumulate whenever usage limits are fetched (TUI footer, /usage, zetacode usage).";
|
|
221
221
|
readonly "providers.utilization": "Window utilization";
|
|
222
222
|
readonly "providers.utilization-description": "{window} used fraction per account, {from} – {to}";
|
|
223
223
|
readonly "providers.utilization-description-plain": "Recorded limit utilization per account";
|
|
@@ -846,7 +846,7 @@ export declare const enLocale: {
|
|
|
846
846
|
readonly "providers.exhaustions-title": "Transitions into an exhausted state observed in range";
|
|
847
847
|
readonly "providers.exhausted-n": "{count} exhausted";
|
|
848
848
|
readonly "providers.no-snapshots-in-range": "No usage snapshots in this range";
|
|
849
|
-
readonly "providers.snapshot-hint": "Usage snapshots accumulate whenever usage limits are fetched (TUI footer, /usage,
|
|
849
|
+
readonly "providers.snapshot-hint": "Usage snapshots accumulate whenever usage limits are fetched (TUI footer, /usage, zetacode usage).";
|
|
850
850
|
readonly "providers.utilization": "Window utilization";
|
|
851
851
|
readonly "providers.utilization-description": "{window} used fraction per account, {from} – {to}";
|
|
852
852
|
readonly "providers.utilization-description-plain": "Recorded limit utilization per account";
|
|
@@ -56,7 +56,7 @@ export declare const providers: {
|
|
|
56
56
|
readonly "providers.exhaustions-title": "Transitions into an exhausted state observed in range";
|
|
57
57
|
readonly "providers.exhausted-n": "{count} exhausted";
|
|
58
58
|
readonly "providers.no-snapshots-in-range": "No usage snapshots in this range";
|
|
59
|
-
readonly "providers.snapshot-hint": "Usage snapshots accumulate whenever usage limits are fetched (TUI footer, /usage,
|
|
59
|
+
readonly "providers.snapshot-hint": "Usage snapshots accumulate whenever usage limits are fetched (TUI footer, /usage, zetacode usage).";
|
|
60
60
|
readonly "providers.utilization": "Window utilization";
|
|
61
61
|
readonly "providers.utilization-description": "{window} used fraction per account, {from} – {to}";
|
|
62
62
|
readonly "providers.utilization-description-plain": "Recorded limit utilization per account";
|
|
@@ -124,7 +124,7 @@ export declare const providers: {
|
|
|
124
124
|
readonly "providers.exhaustions-title": "该时间范围内观测到的进入耗尽状态的次数";
|
|
125
125
|
readonly "providers.exhausted-n": "{count} 次耗尽";
|
|
126
126
|
readonly "providers.no-snapshots-in-range": "该时间范围内没有用量快照";
|
|
127
|
-
readonly "providers.snapshot-hint": "每当获取用量限额时都会累积用量快照(TUI 底部、/usage、
|
|
127
|
+
readonly "providers.snapshot-hint": "每当获取用量限额时都会累积用量快照(TUI 底部、/usage、zetacode usage)。";
|
|
128
128
|
readonly "providers.utilization": "窗口利用率";
|
|
129
129
|
readonly "providers.utilization-description": "{window} 各账号的已用比例,{from} – {to}";
|
|
130
130
|
readonly "providers.utilization-description-plain": "各账号已记录的限额利用率";
|
package/dist/types/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export { formatStatsDashboardUrl, type StartServerOptions, startServer } from ".
|
|
|
8
8
|
export type { GainDashboardStats, GainSource, GainSourceTotals, GainTimeSeriesPoint } from "./shared-types.js";
|
|
9
9
|
export type { AggregatedStats, DashboardStats, FolderStats, MessageStats, ModelPerformancePoint, ModelStats, ModelTimeSeriesPoint, TimeSeriesPoint, ToolDashboardStats, ToolModelStats, ToolTimeSeriesPoint, ToolUsageStats, } from "./types.js";
|
|
10
10
|
/**
|
|
11
|
-
* Print the dashboard summary to the console. Shared by `
|
|
11
|
+
* Print the dashboard summary to the console. Shared by `zetacode stats --summary`
|
|
12
12
|
* and the standalone `zeta-stats --sync`.
|
|
13
13
|
*/
|
|
14
14
|
export declare function printStatsSummary(): Promise<void>;
|
package/dist/types/parser.d.ts
CHANGED
|
@@ -75,7 +75,17 @@ export declare function parseSessionFile(sessionPath: string, fromOffset?: numbe
|
|
|
75
75
|
*/
|
|
76
76
|
export declare function listSessionFolders(): Promise<string[]>;
|
|
77
77
|
/**
|
|
78
|
-
*
|
|
78
|
+
* Name prefix of the scratch directories `zetacode gc --compress` creates beside
|
|
79
|
+
* the files it copies on APFS. Their `.jsonl` files are transient copies of
|
|
80
|
+
* transcripts, never transcripts of their own; a killed run can leave one
|
|
81
|
+
* behind until the next `zetacode gc --compress --apply` removes it.
|
|
82
|
+
*/
|
|
83
|
+
export declare const COMPRESS_SCRATCH_PREFIX = ".omp-gc-compress-";
|
|
84
|
+
/** Whether `relativePath` (from any ancestor directory) lies inside a compress scratch directory. */
|
|
85
|
+
export declare function isCompressScratchPath(relativePath: string): boolean;
|
|
86
|
+
/**
|
|
87
|
+
* List all session files in a folder, excluding copies inside
|
|
88
|
+
* `zetacode gc --compress` scratch directories.
|
|
79
89
|
*/
|
|
80
90
|
export declare function listSessionFiles(folderPath: string): Promise<string[]>;
|
|
81
91
|
/**
|
|
@@ -416,7 +416,7 @@ export interface UsageWindowSeries {
|
|
|
416
416
|
accountKey: string;
|
|
417
417
|
/** Email/account id when known, else the stable account key. */
|
|
418
418
|
accountLabel: string;
|
|
419
|
-
/** Groups the same limit window across accounts
|
|
419
|
+
/** Groups the same limit window across accounts: the limit id, or quota family + window for positional Codex slots. */
|
|
420
420
|
windowKey: string;
|
|
421
421
|
/** Human label of the limit (distinguishes same-duration windows). */
|
|
422
422
|
windowLabel: string;
|
|
@@ -429,7 +429,7 @@ export interface UsageWindowSeries {
|
|
|
429
429
|
*/
|
|
430
430
|
export interface ProviderWindowInsight {
|
|
431
431
|
provider: string;
|
|
432
|
-
/** Groups the same limit window across accounts
|
|
432
|
+
/** Groups the same limit window across accounts: the limit id, or quota family + window for positional Codex slots. */
|
|
433
433
|
windowKey: string;
|
|
434
434
|
/** Human label of the limit (distinguishes same-duration windows). */
|
|
435
435
|
windowLabel: string;
|
|
@@ -57,10 +57,12 @@ export declare function sumFleetTokens(clients: ClientUsageClientSummary[]): Map
|
|
|
57
57
|
/**
|
|
58
58
|
* Derive utilization series and per-window insights from raw snapshots.
|
|
59
59
|
*
|
|
60
|
-
* Windows are grouped by `(provider,
|
|
61
|
-
* Distinct limits can share a duration label (Anthropic's overall
|
|
62
|
-
* model-scoped 7-day windows, Codex's base and Spark weeklies); merging
|
|
63
|
-
* interleaves unrelated fractions per account and wildly inflates
|
|
60
|
+
* Windows are grouped by `(provider, windowKeyOf(row))` — never by display
|
|
61
|
+
* label alone. Distinct limits can share a duration label (Anthropic's overall
|
|
62
|
+
* and model-scoped 7-day windows, Codex's base and Spark weeklies); merging
|
|
63
|
+
* them interleaves unrelated fractions per account and wildly inflates
|
|
64
|
+
* consumption. Conversely, one window reported under different positional
|
|
65
|
+
* slots by different accounts must merge (see {@link windowKeyOf}).
|
|
64
66
|
*
|
|
65
67
|
* `tokensByProvider` supplies each provider's token burn over the same time
|
|
66
68
|
* range (fleet-wide broker client reports when available, else local message
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@linxiraos/pi-stats",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.3.0",
|
|
5
5
|
"description": "Local observability dashboard for omp AI usage statistics",
|
|
6
6
|
"homepage": "https://linxira-os.github.io/zeta/",
|
|
7
7
|
"author": "Linxira-OS",
|
|
@@ -33,23 +33,23 @@
|
|
|
33
33
|
"build": "bun run build.ts",
|
|
34
34
|
"dev": "bun run src/index.ts",
|
|
35
35
|
"check": "oxlint . && oxfmt --check --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts' && bun run check:types",
|
|
36
|
-
"check:types": "
|
|
36
|
+
"check:types": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json --noEmit",
|
|
37
37
|
"lint": "oxlint .",
|
|
38
38
|
"fix": "oxlint --fix --fix-suggestions . && bun run fmt",
|
|
39
39
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@linxiraos/pi-ai": "1.
|
|
43
|
-
"@linxiraos/pi-catalog": "1.
|
|
44
|
-
"@linxiraos/pi-utils": "1.
|
|
42
|
+
"@linxiraos/pi-ai": "1.3.0",
|
|
43
|
+
"@linxiraos/pi-catalog": "1.3.0",
|
|
44
|
+
"@linxiraos/pi-utils": "1.3.0",
|
|
45
45
|
"lucide-react": "^1.24.0",
|
|
46
46
|
"react": "19.2.7",
|
|
47
47
|
"react-dom": "19.2.7"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@types/bun": "
|
|
51
|
-
"@types/react": "
|
|
52
|
-
"@types/react-dom": "
|
|
50
|
+
"@types/bun": "1.4.3",
|
|
51
|
+
"@types/react": "19.3.0",
|
|
52
|
+
"@types/react-dom": "19.3.0"
|
|
53
53
|
},
|
|
54
54
|
"engines": {
|
|
55
55
|
"bun": ">=1.3.14"
|
package/src/client/i18n/index.ts
CHANGED
|
@@ -31,7 +31,7 @@ export function setLocale(tag: string | undefined | null): StatsLocale {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* The locale to boot with: the host's language when `
|
|
34
|
+
* The locale to boot with: the host's language when `zetacode stats` injected it,
|
|
35
35
|
* then the browser's own preference, then English.
|
|
36
36
|
*/
|
|
37
37
|
export function resolveInitialLocale(): StatsLocale {
|
|
@@ -69,7 +69,7 @@ export const providers = {
|
|
|
69
69
|
"providers.exhausted-n": "{count} exhausted",
|
|
70
70
|
"providers.no-snapshots-in-range": "No usage snapshots in this range",
|
|
71
71
|
"providers.snapshot-hint":
|
|
72
|
-
"Usage snapshots accumulate whenever usage limits are fetched (TUI footer, /usage,
|
|
72
|
+
"Usage snapshots accumulate whenever usage limits are fetched (TUI footer, /usage, zetacode usage).",
|
|
73
73
|
// ── window utilization card + accounts table ────────────────────────
|
|
74
74
|
"providers.utilization": "Window utilization",
|
|
75
75
|
"providers.utilization-description": "{window} used fraction per account, {from} – {to}",
|
|
@@ -146,7 +146,7 @@ export const providers = {
|
|
|
146
146
|
"providers.exhaustions-title": "该时间范围内观测到的进入耗尽状态的次数",
|
|
147
147
|
"providers.exhausted-n": "{count} 次耗尽",
|
|
148
148
|
"providers.no-snapshots-in-range": "该时间范围内没有用量快照",
|
|
149
|
-
"providers.snapshot-hint": "每当获取用量限额时都会累积用量快照(TUI 底部、/usage、
|
|
149
|
+
"providers.snapshot-hint": "每当获取用量限额时都会累积用量快照(TUI 底部、/usage、zetacode usage)。",
|
|
150
150
|
// ── window utilization card + accounts table ────────────────────────
|
|
151
151
|
"providers.utilization": "窗口利用率",
|
|
152
152
|
"providers.utilization-description": "{window} 各账号的已用比例,{from} – {to}",
|
|
@@ -197,7 +197,7 @@ interface JudgePanelProps {
|
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
const UNAVAILABLE_HINT =
|
|
200
|
-
"Judging needs the zeta host process: open the dashboard with
|
|
200
|
+
"Judging needs the zeta host process: open the dashboard with zetacode stats and configure a judge model. Until then messages are classified by regex signals.";
|
|
201
201
|
|
|
202
202
|
function JudgePanel({ active, range, judgeAvailable, job, onRunStarted, onRunChanged }: JudgePanelProps) {
|
|
203
203
|
const [modalOpen, setModalOpen] = useState(false);
|
package/src/db.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import type { ServiceTier, Usage } from "@linxiraos/pi-ai";
|
|
4
|
+
import { pricingPeerFor } from "@linxiraos/pi-catalog/compat/behavior";
|
|
4
5
|
import {
|
|
5
6
|
calculateUncachedInputCost,
|
|
6
7
|
calculateUsageCost,
|
|
7
8
|
type GeneratedProvider,
|
|
8
9
|
getBundledModel,
|
|
10
|
+
hasRecurringTariff,
|
|
9
11
|
} from "@linxiraos/pi-catalog/models";
|
|
10
12
|
import type { ModelCost } from "@linxiraos/pi-catalog/types";
|
|
11
13
|
import { getConfigRootDir, getStatsDbPath, VERSION } from "@linxiraos/pi-utils";
|
|
@@ -395,9 +397,16 @@ function getCatalogCost(provider: string, modelId: string): ModelCost | null {
|
|
|
395
397
|
return primaryCost;
|
|
396
398
|
}
|
|
397
399
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
400
|
+
// openai-codex has no catalog pricing-peer row (its bundled rows carry
|
|
401
|
+
// their own prices); keep its first-party fallback here.
|
|
402
|
+
const peer = pricingPeerFor(provider, modelId);
|
|
403
|
+
const fallbacks = peer
|
|
404
|
+
? peer.peers.map(peerProvider => [peerProvider, peer.peerId] as const)
|
|
405
|
+
: provider === "openai-codex"
|
|
406
|
+
? [["openai", modelId] as const]
|
|
407
|
+
: [];
|
|
408
|
+
for (const [fallbackProvider, fallbackId] of fallbacks) {
|
|
409
|
+
const fallbackCost = getBundledModelCost(fallbackProvider, fallbackId);
|
|
401
410
|
if (fallbackCost && hasBillableCost(fallbackCost)) {
|
|
402
411
|
return fallbackCost;
|
|
403
412
|
}
|
|
@@ -473,9 +482,11 @@ function resolveStoredCost(stats: MessageStatsInput): ResolvedCost {
|
|
|
473
482
|
const storedCost = raw ? normalizeUsageCost(raw) : undefined;
|
|
474
483
|
const catalogCost = getCatalogCost(stats.provider, stats.model);
|
|
475
484
|
|
|
476
|
-
//
|
|
477
|
-
// Preserve legacy zero-cost subscription correction for
|
|
478
|
-
|
|
485
|
+
// Recurring-tariff prices are frozen per request, including explicitly free
|
|
486
|
+
// usage. Preserve legacy zero-cost subscription correction for other models;
|
|
487
|
+
// dated rate cards reprice it at the card for the request's timestamp.
|
|
488
|
+
const recurring = catalogCost?.timeBased !== undefined && hasRecurringTariff(catalogCost.timeBased);
|
|
489
|
+
if (storedCost && Number.isFinite(storedCost.total) && (storedCost.total !== 0 || recurring)) {
|
|
479
490
|
return { cost: storedCost, unpriced: false };
|
|
480
491
|
}
|
|
481
492
|
|
|
@@ -538,12 +549,14 @@ function backfillMissingCatalogCosts(database: Database): void {
|
|
|
538
549
|
const applyBackfill = database.transaction(() => {
|
|
539
550
|
for (const row of rows) {
|
|
540
551
|
// A stored zero cannot distinguish missing historical prices from an
|
|
541
|
-
// explicitly free request. Never reprice recorded
|
|
542
|
-
// zero ingested for a
|
|
552
|
+
// explicitly free request. Never reprice recorded recurring-tariff
|
|
553
|
+
// usage: a zero ingested for such a card is permanent, because this
|
|
543
554
|
// backfill skips those rows. Re-parsing the session file is the only
|
|
544
555
|
// repair, and it works only once the entry's absent `cost` reaches
|
|
545
556
|
// `resolveStoredCost` as absence rather than as a synthesized zero.
|
|
546
|
-
|
|
557
|
+
// Dated rate cards need the row's timestamp to pick their card.
|
|
558
|
+
const schedule = getCatalogCost(row.provider, row.model)?.timeBased;
|
|
559
|
+
if (schedule && (hasRecurringTariff(schedule) || !hasRequestTimestamp(row.timestamp))) continue;
|
|
547
560
|
const cost = calculateCatalogCost(
|
|
548
561
|
row.provider,
|
|
549
562
|
row.model,
|
package/src/frustration.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { type Judge, judgeQuestions, type Model } from "@linxiraos/pi-ai";
|
|
20
20
|
import { compareRevision, parseRevision } from "@linxiraos/pi-catalog/compat/revision";
|
|
21
21
|
import { classifyModel } from "@linxiraos/pi-catalog/compat/taxonomy";
|
|
22
|
+
import { getEffectiveTokenCost } from "@linxiraos/pi-catalog/models";
|
|
22
23
|
import { logger } from "@linxiraos/pi-utils";
|
|
23
24
|
import { getTimeRangeConfig } from "./aggregator";
|
|
24
25
|
import {
|
|
@@ -78,7 +79,7 @@ const ATTEMPTS_PER_TEXT = 3;
|
|
|
78
79
|
const CIRCUIT_BREAKER_FAILURES = 25;
|
|
79
80
|
|
|
80
81
|
const NO_PROVIDER_REASON =
|
|
81
|
-
"This dashboard was started without a judge (standalone zetacode stats). Run `
|
|
82
|
+
"This dashboard was started without a judge (standalone zetacode stats). Run `zetacode stats` to classify.";
|
|
82
83
|
const NO_MODEL_REASON = "No judge model is available. Configure the `judge` model role.";
|
|
83
84
|
|
|
84
85
|
let judgeProvider: StatsJudgeProvider | undefined;
|
|
@@ -216,8 +217,8 @@ export async function estimateFrustrationRun(range?: string | null): Promise<Fru
|
|
|
216
217
|
const { cutoff } = getTimeRangeConfig(range);
|
|
217
218
|
const { messages, chars } = getPendingFrustrationTotals(cutoff);
|
|
218
219
|
const inputTokens = messages * REQUEST_OVERHEAD_TOKENS + Math.ceil(chars / CHARS_PER_TOKEN);
|
|
219
|
-
const
|
|
220
|
-
|
|
220
|
+
const rates = getEffectiveTokenCost(model.cost);
|
|
221
|
+
const cost = (inputTokens * rates.input) / 1e6 + (messages * OUTPUT_TOKENS_PER_REQUEST * rates.output) / 1e6;
|
|
221
222
|
return { available: true, messages, chars, inputTokens, cost, judge: `${model.provider}/${model.id}` };
|
|
222
223
|
}
|
|
223
224
|
|
package/src/index.ts
CHANGED
|
@@ -48,7 +48,7 @@ function formatCost(n: number, unpricedRequests = 0): string {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
* Print the dashboard summary to the console. Shared by `
|
|
51
|
+
* Print the dashboard summary to the console. Shared by `zetacode stats --summary`
|
|
52
52
|
* and the standalone `zeta-stats --sync`.
|
|
53
53
|
*/
|
|
54
54
|
export async function printStatsSummary(): Promise<void> {
|
package/src/live.ts
CHANGED
|
@@ -15,6 +15,7 @@ import * as path from "node:path";
|
|
|
15
15
|
import { getSessionsDir, logger } from "@linxiraos/pi-utils";
|
|
16
16
|
import { syncAllSessions } from "./aggregator";
|
|
17
17
|
import { getDataVersion, initDb } from "./db";
|
|
18
|
+
import { isCompressScratchPath } from "./parser";
|
|
18
19
|
import { getRollupStatus, refreshRollups } from "./rollup";
|
|
19
20
|
import type { LiveStatus, LiveSyncStatus } from "./shared-types";
|
|
20
21
|
|
|
@@ -231,7 +232,7 @@ export class StatsLive {
|
|
|
231
232
|
const pending = new Set<string>();
|
|
232
233
|
try {
|
|
233
234
|
this.#watcher = fs.watch(root, { recursive: true }, (_event, filename) => {
|
|
234
|
-
if (!filename?.endsWith(".jsonl")) return;
|
|
235
|
+
if (!filename?.endsWith(".jsonl") || isCompressScratchPath(filename)) return;
|
|
235
236
|
pending.add(path.join(root, filename));
|
|
236
237
|
clearTimeout(this.#debounceTimer ?? undefined);
|
|
237
238
|
this.#debounceTimer = setTimeout(() => {
|
package/src/parser.ts
CHANGED
|
@@ -687,12 +687,33 @@ export async function listSessionFolders(): Promise<string[]> {
|
|
|
687
687
|
}
|
|
688
688
|
|
|
689
689
|
/**
|
|
690
|
-
*
|
|
690
|
+
* Name prefix of the scratch directories `zetacode gc --compress` creates beside
|
|
691
|
+
* the files it copies on APFS. Their `.jsonl` files are transient copies of
|
|
692
|
+
* transcripts, never transcripts of their own; a killed run can leave one
|
|
693
|
+
* behind until the next `zetacode gc --compress --apply` removes it.
|
|
694
|
+
*/
|
|
695
|
+
export const COMPRESS_SCRATCH_PREFIX = ".omp-gc-compress-";
|
|
696
|
+
|
|
697
|
+
/** Whether `relativePath` (from any ancestor directory) lies inside a compress scratch directory. */
|
|
698
|
+
export function isCompressScratchPath(relativePath: string): boolean {
|
|
699
|
+
return relativePath.split(/[\\/]/).some(part => part.startsWith(COMPRESS_SCRATCH_PREFIX));
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* List all session files in a folder, excluding copies inside
|
|
704
|
+
* `zetacode gc --compress` scratch directories.
|
|
691
705
|
*/
|
|
692
706
|
export async function listSessionFiles(folderPath: string): Promise<string[]> {
|
|
693
707
|
try {
|
|
694
708
|
const entries = await fs.readdir(folderPath, { recursive: true, withFileTypes: true });
|
|
695
|
-
return entries
|
|
709
|
+
return entries
|
|
710
|
+
.filter(
|
|
711
|
+
e =>
|
|
712
|
+
e.isFile() &&
|
|
713
|
+
e.name.endsWith(".jsonl") &&
|
|
714
|
+
!isCompressScratchPath(path.relative(folderPath, e.parentPath)),
|
|
715
|
+
)
|
|
716
|
+
.map(e => path.join(e.parentPath, e.name));
|
|
696
717
|
} catch {
|
|
697
718
|
return [];
|
|
698
719
|
}
|
package/src/port-conflict.ts
CHANGED
|
@@ -11,8 +11,6 @@ const STATS_RUNTIME_IMAGES: Record<string, true> = {
|
|
|
11
11
|
bun: true,
|
|
12
12
|
node: true,
|
|
13
13
|
// Zeta runtime images: the compiled CLI and the standalone npm bin.
|
|
14
|
-
"zeta-c": true,
|
|
15
|
-
"zeta-cli": true,
|
|
16
14
|
zetacode: true,
|
|
17
15
|
"zeta-stats": true,
|
|
18
16
|
// Upstream OMP images kept so a leftover upstream dashboard is still reclaimable.
|
|
@@ -257,12 +255,9 @@ async function reclaimStatsPort(port: number, hasDashboardIdentity = false): Pro
|
|
|
257
255
|
normalizedImage === "omp-stats" ||
|
|
258
256
|
/(?:^|[/"'\s])(?:zeta|omp)-stats(?:\.exe)?(?:["'\s]|$)/.test(normalizedCommand) ||
|
|
259
257
|
/\/packages\/stats\/src\/index\.ts(?:["'\s]|$)/.test(normalizedCommand) ||
|
|
260
|
-
((normalizedImage === "
|
|
261
|
-
normalizedImage === "zeta-cli" ||
|
|
262
|
-
normalizedImage === "zetacode" ||
|
|
263
|
-
normalizedImage === "omp") &&
|
|
258
|
+
((normalizedImage === "zetacode" || normalizedImage === "omp") &&
|
|
264
259
|
/(?:^|\s)stats(?:\s|$)/.test(normalizedCommand)) ||
|
|
265
|
-
/(?:^|\/)(?:
|
|
260
|
+
/(?:^|\/)(?:zetacode|omp)(?:\.exe)?["'\s]+stats(?:["'\s]|$)/.test(normalizedCommand);
|
|
266
261
|
if (!STATS_RUNTIME_IMAGES[normalizedImage] || (!hasStatsIdentity && !hasDashboardIdentity)) {
|
|
267
262
|
throw new Error(
|
|
268
263
|
`Port ${port} is in use by ${holder.image} (PID ${holder.pid}), which is not identifiable as a zetacode stats dashboard; refusing to stop it.`,
|
package/src/shared-types.ts
CHANGED
|
@@ -445,7 +445,7 @@ export interface UsageWindowSeries {
|
|
|
445
445
|
accountKey: string;
|
|
446
446
|
/** Email/account id when known, else the stable account key. */
|
|
447
447
|
accountLabel: string;
|
|
448
|
-
/** Groups the same limit window across accounts
|
|
448
|
+
/** Groups the same limit window across accounts: the limit id, or quota family + window for positional Codex slots. */
|
|
449
449
|
windowKey: string;
|
|
450
450
|
/** Human label of the limit (distinguishes same-duration windows). */
|
|
451
451
|
windowLabel: string;
|
|
@@ -459,7 +459,7 @@ export interface UsageWindowSeries {
|
|
|
459
459
|
*/
|
|
460
460
|
export interface ProviderWindowInsight {
|
|
461
461
|
provider: string;
|
|
462
|
-
/** Groups the same limit window across accounts
|
|
462
|
+
/** Groups the same limit window across accounts: the limit id, or quota family + window for positional Codex slots. */
|
|
463
463
|
windowKey: string;
|
|
464
464
|
/** Human label of the limit (distinguishes same-duration windows). */
|
|
465
465
|
windowLabel: string;
|
package/src/trace.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import * as fs from "node:fs/promises";
|
|
13
13
|
import * as path from "node:path";
|
|
14
|
+
import { pricingPeerFor } from "@linxiraos/pi-catalog/compat/behavior";
|
|
14
15
|
import { getBundledModel, type GeneratedProvider } from "@linxiraos/pi-catalog/models";
|
|
15
16
|
import { getSessionsDir, isEnoent } from "@linxiraos/pi-utils";
|
|
16
17
|
import { initDb, isScheduledCatalogModel } from "./db";
|
|
@@ -393,11 +394,13 @@ function scanTranscript(
|
|
|
393
394
|
if (totalTokens > 0 && (costTotal === undefined || costTotal === 0)) {
|
|
394
395
|
const provider = typeof msg.provider === "string" ? msg.provider : "";
|
|
395
396
|
const model = typeof msg.model === "string" ? msg.model : "";
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
397
|
+
const peer = provider && model ? pricingPeerFor(provider, model) : undefined;
|
|
398
|
+
if (peer) {
|
|
399
|
+
const priced = peer.peers.some(peerProvider => {
|
|
400
|
+
const ref = getBundledModel(peerProvider as GeneratedProvider, peer.peerId)?.cost;
|
|
401
|
+
return ref && (ref.input !== 0 || ref.output !== 0 || ref.cacheRead !== 0 || ref.cacheWrite !== 0);
|
|
402
|
+
});
|
|
403
|
+
if (!priced) unpricedRequests++;
|
|
401
404
|
} else if (costObj == null && provider && model) {
|
|
402
405
|
const hasTimestamp =
|
|
403
406
|
(typeof msg.timestamp === "number" && Number.isFinite(msg.timestamp) && msg.timestamp > 0) ||
|
package/src/usage-windows.ts
CHANGED
|
@@ -241,13 +241,33 @@ function windowDisplayLabel(row: UsageSnapshotRow): string {
|
|
|
241
241
|
return `${label} · ${windowLabel}`;
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
/** Trailing limit-id segment naming the report slot a window arrived in, not the quota itself. */
|
|
245
|
+
const POSITIONAL_SLOT = /:(?:primary|secondary)$/;
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Cross-account identity of one limit window: usually the limit id. Positional
|
|
249
|
+
* ids (Codex's `openai-codex:primary`) name a slot, not a window — a Team plan
|
|
250
|
+
* reports its 5-hour window as `primary` and its 7-day one as `secondary`,
|
|
251
|
+
* while a weekly-only plan reports its 7-day window as `primary`. For those,
|
|
252
|
+
* the quota family (the id minus the slot) plus the recorded window label is
|
|
253
|
+
* the identity, so both plans' weeklies share a group while Spark's
|
|
254
|
+
* (`openai-codex:spark:*`) stays apart. A row without a window label keeps its
|
|
255
|
+
* raw id rather than having its window guessed from the slot.
|
|
256
|
+
*/
|
|
257
|
+
function windowKeyOf(row: UsageSnapshotRow): string {
|
|
258
|
+
if (!row.windowLabel || !POSITIONAL_SLOT.test(row.limitId)) return row.limitId;
|
|
259
|
+
return `${row.limitId.replace(POSITIONAL_SLOT, "")}:${row.windowLabel}`;
|
|
260
|
+
}
|
|
261
|
+
|
|
244
262
|
/**
|
|
245
263
|
* Derive utilization series and per-window insights from raw snapshots.
|
|
246
264
|
*
|
|
247
|
-
* Windows are grouped by `(provider,
|
|
248
|
-
* Distinct limits can share a duration label (Anthropic's overall
|
|
249
|
-
* model-scoped 7-day windows, Codex's base and Spark weeklies); merging
|
|
250
|
-
* interleaves unrelated fractions per account and wildly inflates
|
|
265
|
+
* Windows are grouped by `(provider, windowKeyOf(row))` — never by display
|
|
266
|
+
* label alone. Distinct limits can share a duration label (Anthropic's overall
|
|
267
|
+
* and model-scoped 7-day windows, Codex's base and Spark weeklies); merging
|
|
268
|
+
* them interleaves unrelated fractions per account and wildly inflates
|
|
269
|
+
* consumption. Conversely, one window reported under different positional
|
|
270
|
+
* slots by different accounts must merge (see {@link windowKeyOf}).
|
|
251
271
|
*
|
|
252
272
|
* `tokensByProvider` supplies each provider's token burn over the same time
|
|
253
273
|
* range (fleet-wide broker client reports when available, else local message
|
|
@@ -262,12 +282,13 @@ export function computeUsageWindowStats(
|
|
|
262
282
|
): UsageWindowStats {
|
|
263
283
|
const groups = new Map<string, WindowGroup>();
|
|
264
284
|
for (const row of rows) {
|
|
265
|
-
const
|
|
285
|
+
const windowKey = windowKeyOf(row);
|
|
286
|
+
const groupKey = `${row.provider}\u0000${windowKey}`;
|
|
266
287
|
let group = groups.get(groupKey);
|
|
267
288
|
if (!group) {
|
|
268
289
|
group = {
|
|
269
290
|
provider: row.provider,
|
|
270
|
-
windowKey
|
|
291
|
+
windowKey,
|
|
271
292
|
windowLabel: windowDisplayLabel(row),
|
|
272
293
|
accounts: new Map(),
|
|
273
294
|
};
|