@oh-my-pi/omp-stats 18.10.3 → 18.11.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 +12 -0
- 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 +4 -4
- package/src/db.ts +11 -6
- package/src/frustration.ts +3 -2
- package/src/live.ts +2 -1
- package/src/parser.ts +23 -2
- package/src/shared-types.ts +2 -2
- package/src/usage-windows.ts +27 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.11.0] - 2026-10-14
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Providers → Subscription windows no longer mixes Codex 5-hour and 7-day quotas across plans: Team and weekly-only (ProLite) accounts now share the 7-day row, the 5-hour row counts only accounts that have one, and labels no longer flip with whichever account reported last ([#15664](https://github.com/can1357/oh-my-pi/issues/15664))
|
|
10
|
+
|
|
11
|
+
## [18.10.4] - 2026-10-13
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Usage recorded at $0 for a model whose catalog price changes on a date (Gemini 4 Argon, Devin SWE-2) is now priced at the rate in force on the request's date, instead of staying $0 like peak/off-peak tariff usage ([#15620](https://github.com/can1357/oh-my-pi/pull/15620) by [@eggpeat](https://github.com/eggpeat))
|
|
16
|
+
|
|
5
17
|
## [18.9.0] - 2026-10-11
|
|
6
18
|
|
|
7
19
|
### Fixed
|
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 `omp 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 `omp 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
|
+
* `omp gc --compress` scratch directories.
|
|
79
89
|
*/
|
|
80
90
|
export declare function listSessionFiles(folderPath: string): Promise<string[]>;
|
|
81
91
|
/**
|
|
@@ -409,7 +409,7 @@ export interface UsageWindowSeries {
|
|
|
409
409
|
accountKey: string;
|
|
410
410
|
/** Email/account id when known, else the stable account key. */
|
|
411
411
|
accountLabel: string;
|
|
412
|
-
/** Groups the same limit window across accounts
|
|
412
|
+
/** Groups the same limit window across accounts: the limit id, or quota family + window for positional Codex slots. */
|
|
413
413
|
windowKey: string;
|
|
414
414
|
/** Human label of the limit (distinguishes same-duration windows). */
|
|
415
415
|
windowLabel: string;
|
|
@@ -422,7 +422,7 @@ export interface UsageWindowSeries {
|
|
|
422
422
|
*/
|
|
423
423
|
export interface ProviderWindowInsight {
|
|
424
424
|
provider: string;
|
|
425
|
-
/** Groups the same limit window across accounts
|
|
425
|
+
/** Groups the same limit window across accounts: the limit id, or quota family + window for positional Codex slots. */
|
|
426
426
|
windowKey: string;
|
|
427
427
|
/** Human label of the limit (distinguishes same-duration windows). */
|
|
428
428
|
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": "@oh-my-pi/omp-stats",
|
|
4
|
-
"version": "18.
|
|
4
|
+
"version": "18.11.0",
|
|
5
5
|
"description": "Local observability dashboard for omp AI usage statistics",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": {
|
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@oh-my-pi/pi-ai": "18.
|
|
46
|
-
"@oh-my-pi/pi-catalog": "18.
|
|
47
|
-
"@oh-my-pi/pi-utils": "18.
|
|
45
|
+
"@oh-my-pi/pi-ai": "18.11.0",
|
|
46
|
+
"@oh-my-pi/pi-catalog": "18.11.0",
|
|
47
|
+
"@oh-my-pi/pi-utils": "18.11.0",
|
|
48
48
|
"lucide-react": "^1.24.0",
|
|
49
49
|
"react": "19.2.7",
|
|
50
50
|
"react-dom": "19.2.7"
|
package/src/db.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
calculateUsageCost,
|
|
7
7
|
type GeneratedProvider,
|
|
8
8
|
getBundledModel,
|
|
9
|
+
hasRecurringTariff,
|
|
9
10
|
} from "@oh-my-pi/pi-catalog/models";
|
|
10
11
|
import type { ModelCost } from "@oh-my-pi/pi-catalog/types";
|
|
11
12
|
import { getConfigRootDir, getStatsDbPath, VERSION } from "@oh-my-pi/pi-utils";
|
|
@@ -463,9 +464,11 @@ function resolveStoredCost(stats: MessageStatsInput): ResolvedCost {
|
|
|
463
464
|
const storedCost = raw ? normalizeUsageCost(raw) : undefined;
|
|
464
465
|
const catalogCost = getCatalogCost(stats.provider, stats.model);
|
|
465
466
|
|
|
466
|
-
//
|
|
467
|
-
// Preserve legacy zero-cost subscription correction for
|
|
468
|
-
|
|
467
|
+
// Recurring-tariff prices are frozen per request, including explicitly free
|
|
468
|
+
// usage. Preserve legacy zero-cost subscription correction for other models;
|
|
469
|
+
// dated rate cards reprice it at the card for the request's timestamp.
|
|
470
|
+
const recurring = catalogCost?.timeBased !== undefined && hasRecurringTariff(catalogCost.timeBased);
|
|
471
|
+
if (storedCost && Number.isFinite(storedCost.total) && (storedCost.total !== 0 || recurring)) {
|
|
469
472
|
return { cost: storedCost, unpriced: false };
|
|
470
473
|
}
|
|
471
474
|
|
|
@@ -528,12 +531,14 @@ function backfillMissingCatalogCosts(database: Database): void {
|
|
|
528
531
|
const applyBackfill = database.transaction(() => {
|
|
529
532
|
for (const row of rows) {
|
|
530
533
|
// A stored zero cannot distinguish missing historical prices from an
|
|
531
|
-
// explicitly free request. Never reprice recorded
|
|
532
|
-
// zero ingested for a
|
|
534
|
+
// explicitly free request. Never reprice recorded recurring-tariff
|
|
535
|
+
// usage: a zero ingested for such a card is permanent, because this
|
|
533
536
|
// backfill skips those rows. Re-parsing the session file is the only
|
|
534
537
|
// repair, and it works only once the entry's absent `cost` reaches
|
|
535
538
|
// `resolveStoredCost` as absence rather than as a synthesized zero.
|
|
536
|
-
|
|
539
|
+
// Dated rate cards need the row's timestamp to pick their card.
|
|
540
|
+
const schedule = getCatalogCost(row.provider, row.model)?.timeBased;
|
|
541
|
+
if (schedule && (hasRecurringTariff(schedule) || !hasRequestTimestamp(row.timestamp))) continue;
|
|
537
542
|
const cost = calculateCatalogCost(
|
|
538
543
|
row.provider,
|
|
539
544
|
row.model,
|
package/src/frustration.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { type Judge, judgeQuestions, type Model } from "@oh-my-pi/pi-ai";
|
|
20
20
|
import { compareRevision, parseRevision } from "@oh-my-pi/pi-catalog/compat/revision";
|
|
21
21
|
import { classifyModel } from "@oh-my-pi/pi-catalog/compat/taxonomy";
|
|
22
|
+
import { getEffectiveTokenCost } from "@oh-my-pi/pi-catalog/models";
|
|
22
23
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
23
24
|
import { getTimeRangeConfig } from "./aggregator";
|
|
24
25
|
import {
|
|
@@ -214,8 +215,8 @@ export async function estimateFrustrationRun(range?: string | null): Promise<Fru
|
|
|
214
215
|
const { cutoff } = getTimeRangeConfig(range);
|
|
215
216
|
const { messages, chars } = getPendingFrustrationTotals(cutoff);
|
|
216
217
|
const inputTokens = messages * REQUEST_OVERHEAD_TOKENS + Math.ceil(chars / CHARS_PER_TOKEN);
|
|
217
|
-
const
|
|
218
|
-
|
|
218
|
+
const rates = getEffectiveTokenCost(model.cost);
|
|
219
|
+
const cost = (inputTokens * rates.input) / 1e6 + (messages * OUTPUT_TOKENS_PER_REQUEST * rates.output) / 1e6;
|
|
219
220
|
return { available: true, messages, chars, inputTokens, cost, judge: `${model.provider}/${model.id}` };
|
|
220
221
|
}
|
|
221
222
|
|
package/src/live.ts
CHANGED
|
@@ -15,6 +15,7 @@ import * as path from "node:path";
|
|
|
15
15
|
import { getSessionsDir, logger } from "@oh-my-pi/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
|
@@ -672,12 +672,33 @@ export async function listSessionFolders(): Promise<string[]> {
|
|
|
672
672
|
}
|
|
673
673
|
|
|
674
674
|
/**
|
|
675
|
-
*
|
|
675
|
+
* Name prefix of the scratch directories `omp gc --compress` creates beside
|
|
676
|
+
* the files it copies on APFS. Their `.jsonl` files are transient copies of
|
|
677
|
+
* transcripts, never transcripts of their own; a killed run can leave one
|
|
678
|
+
* behind until the next `omp gc --compress --apply` removes it.
|
|
679
|
+
*/
|
|
680
|
+
export const COMPRESS_SCRATCH_PREFIX = ".omp-gc-compress-";
|
|
681
|
+
|
|
682
|
+
/** Whether `relativePath` (from any ancestor directory) lies inside a compress scratch directory. */
|
|
683
|
+
export function isCompressScratchPath(relativePath: string): boolean {
|
|
684
|
+
return relativePath.split(/[\\/]/).some(part => part.startsWith(COMPRESS_SCRATCH_PREFIX));
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* List all session files in a folder, excluding copies inside
|
|
689
|
+
* `omp gc --compress` scratch directories.
|
|
676
690
|
*/
|
|
677
691
|
export async function listSessionFiles(folderPath: string): Promise<string[]> {
|
|
678
692
|
try {
|
|
679
693
|
const entries = await fs.readdir(folderPath, { recursive: true, withFileTypes: true });
|
|
680
|
-
return entries
|
|
694
|
+
return entries
|
|
695
|
+
.filter(
|
|
696
|
+
e =>
|
|
697
|
+
e.isFile() &&
|
|
698
|
+
e.name.endsWith(".jsonl") &&
|
|
699
|
+
!isCompressScratchPath(path.relative(folderPath, e.parentPath)),
|
|
700
|
+
)
|
|
701
|
+
.map(e => path.join(e.parentPath, e.name));
|
|
681
702
|
} catch {
|
|
682
703
|
return [];
|
|
683
704
|
}
|
package/src/shared-types.ts
CHANGED
|
@@ -438,7 +438,7 @@ export interface UsageWindowSeries {
|
|
|
438
438
|
accountKey: string;
|
|
439
439
|
/** Email/account id when known, else the stable account key. */
|
|
440
440
|
accountLabel: string;
|
|
441
|
-
/** Groups the same limit window across accounts
|
|
441
|
+
/** Groups the same limit window across accounts: the limit id, or quota family + window for positional Codex slots. */
|
|
442
442
|
windowKey: string;
|
|
443
443
|
/** Human label of the limit (distinguishes same-duration windows). */
|
|
444
444
|
windowLabel: string;
|
|
@@ -452,7 +452,7 @@ export interface UsageWindowSeries {
|
|
|
452
452
|
*/
|
|
453
453
|
export interface ProviderWindowInsight {
|
|
454
454
|
provider: string;
|
|
455
|
-
/** Groups the same limit window across accounts
|
|
455
|
+
/** Groups the same limit window across accounts: the limit id, or quota family + window for positional Codex slots. */
|
|
456
456
|
windowKey: string;
|
|
457
457
|
/** Human label of the limit (distinguishes same-duration windows). */
|
|
458
458
|
windowLabel: string;
|
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
|
};
|