@agent-finops/core 0.9.5 → 0.9.6
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/dist/actionVerification.d.ts +54 -54
- package/dist/activitySnapshot.d.ts +57 -57
- package/dist/activitySnapshot.js +12 -3
- package/dist/agentEconomicsReceipt.d.ts +62 -62
- package/dist/glance.js +10 -6
- package/dist/localAgentLogs.d.ts +19 -3
- package/dist/localAgentLogs.js +112 -18
- package/dist/modelPricing.d.ts +17 -4
- package/dist/modelPricing.js +114 -22
- package/dist/projectIndexStore.d.ts +14 -12
- package/dist/qualitativeIndexCache.d.ts +12 -10
- package/dist/qualitativeIndexCache.js +5 -0
- package/package.json +1 -1
package/dist/localAgentLogs.js
CHANGED
|
@@ -22,7 +22,15 @@ export function latestObservedWorkingDirectory(calls) {
|
|
|
22
22
|
.sort((left, right) => right.timestamp.localeCompare(left.timestamp))[0]
|
|
23
23
|
?.workingDirectory;
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
// Bumped to 2 when session-cumulative Codex calls gained `maxRequestPromptTokens`
|
|
26
|
+
// (largest single request in the session). Financial caches written by v1 lack
|
|
27
|
+
// this per-request tier evidence, so a >272K-cumulative Codex session would
|
|
28
|
+
// stay voided to "missing" on reuse; a version mismatch re-parses instead.
|
|
29
|
+
// Bumped to 3 when user forks (`forked_from_id`) started resetting their
|
|
30
|
+
// inherited baseline: v2 entries priced a fork's replayed parent history as
|
|
31
|
+
// the child's own usage, so those cached amounts are overstated and must not
|
|
32
|
+
// be reused.
|
|
33
|
+
export const localAgentFinancialParserVersion = 3;
|
|
26
34
|
/**
|
|
27
35
|
* Qualitative parser contract version. Bumped to 2 with the checkpointed
|
|
28
36
|
* streaming path (A4b): entries and checkpoints written by the pre-streaming
|
|
@@ -33,9 +41,15 @@ export const localAgentFinancialParserVersion = 1;
|
|
|
33
41
|
* cross-file completion evidence (`subagentCompletions`, from both Task tool
|
|
34
42
|
* results and background task-notifications): entries persisted by the
|
|
35
43
|
* collapsing parser must re-parse rather than silently keep merging subagent
|
|
36
|
-
* runs into their parent session.
|
|
44
|
+
* runs into their parent session. Bumped to 5 when Codex reducer checkpoints
|
|
45
|
+
* gained `maxTurnPromptTokens` (largest single request): a checkpoint written
|
|
46
|
+
* by v4 lacks the running per-request maximum, so restoring it could under-read
|
|
47
|
+
* the tier evidence for a cumulative session that crossed the boundary — a
|
|
48
|
+
* version mismatch discards it and re-parses from scratch instead. Bumped to 6
|
|
49
|
+
* with the user-fork inherited-baseline fix: v5 entries and checkpoints treated
|
|
50
|
+
* a fork's replayed parent history as the child's own usage and activity.
|
|
37
51
|
*/
|
|
38
|
-
export const localAgentQualitativeParserVersion =
|
|
52
|
+
export const localAgentQualitativeParserVersion = 6;
|
|
39
53
|
/**
|
|
40
54
|
* Conservative launch defaults for action-capable qualitative evidence.
|
|
41
55
|
* Callers must still inspect `qualitativeCoverage` before deriving a finding:
|
|
@@ -384,6 +398,42 @@ function parseCodexTurnUsage(value) {
|
|
|
384
398
|
}
|
|
385
399
|
};
|
|
386
400
|
}
|
|
401
|
+
/**
|
|
402
|
+
* The tier-relevant prompt size of one Codex `last_token_usage` turn: its full
|
|
403
|
+
* request input (Codex `input_tokens` already includes cached input), which is
|
|
404
|
+
* exactly `effectivePromptTokens` of the parsed turn usage. Returned only when
|
|
405
|
+
* the field parses to a non-negative number, so an unreadable turn never lowers
|
|
406
|
+
* a running maximum. Used to prove whether any single request in a cumulative
|
|
407
|
+
* session crossed a per-request tier threshold.
|
|
408
|
+
*/
|
|
409
|
+
function codexTurnPromptTokens(turn) {
|
|
410
|
+
const input = tokenComponentOf(turn.input_tokens);
|
|
411
|
+
return input === undefined ? undefined : input;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Whether a Codex rollout replays another session's history before its own
|
|
415
|
+
* work, so the cumulative counter at its first real task is an inherited
|
|
416
|
+
* baseline rather than this session's usage.
|
|
417
|
+
*
|
|
418
|
+
* TWO kinds of rollout do this, and only the first was recognized before:
|
|
419
|
+
* - subagent rollouts (`thread_source: "subagent"`, or a `source.subagent`);
|
|
420
|
+
* - USER FORKS (`forked_from_id`), which carry `thread_source: "user"` and
|
|
421
|
+
* replay the parent transcript verbatim. Observed: a forked rollout whose
|
|
422
|
+
* first 9,051 of 9,442 token_count events are byte-identical copies of the
|
|
423
|
+
* parent's, all with earlier parent timestamps, carrying 1,204,265,192 of
|
|
424
|
+
* its 1,256,637,395 final cumulative input tokens (95.9%) — parent usage
|
|
425
|
+
* that was being billed again under the child, and again down each fork
|
|
426
|
+
* chain.
|
|
427
|
+
*
|
|
428
|
+
* This is deliberately NOT folded into `isSubagent`: that flag also drives
|
|
429
|
+
* activity attribution and checkpoint identity, where a user fork is a normal
|
|
430
|
+
* user session and must not be relabelled a subagent.
|
|
431
|
+
*/
|
|
432
|
+
function codexHasInheritedHistory(payload) {
|
|
433
|
+
return stringOf(payload.thread_source) === "subagent" ||
|
|
434
|
+
isRecord(payload.source) && "subagent" in payload.source ||
|
|
435
|
+
stringOf(payload.forked_from_id) !== undefined;
|
|
436
|
+
}
|
|
387
437
|
/** Parse one Claude Code transcript (JSONL). Exported for tests. */
|
|
388
438
|
export function parseClaudeCodeTranscript(content, filePath = "", sinceMs, onDiagnostic) {
|
|
389
439
|
const calls = [];
|
|
@@ -634,6 +684,7 @@ function createCodexRolloutParserState() {
|
|
|
634
684
|
fileCounts: new Map(),
|
|
635
685
|
toolCallCount: 0,
|
|
636
686
|
isSubagent: false,
|
|
687
|
+
hasInheritedHistory: false,
|
|
637
688
|
malformedLines: 0
|
|
638
689
|
};
|
|
639
690
|
}
|
|
@@ -666,6 +717,7 @@ function consumeCodexRolloutLine(state, line, onEntry) {
|
|
|
666
717
|
state.rootStartedAtMs = timestampMilliseconds(payload.timestamp ?? entry.timestamp);
|
|
667
718
|
state.isSubagent = stringOf(payload.thread_source) === "subagent" ||
|
|
668
719
|
isRecord(payload.source) && "subagent" in payload.source;
|
|
720
|
+
state.hasInheritedHistory = codexHasInheritedHistory(payload);
|
|
669
721
|
state.parentSessionId = stringOf(payload.parent_thread_id);
|
|
670
722
|
}
|
|
671
723
|
if (entry.type === "turn_context" && payload) {
|
|
@@ -677,7 +729,7 @@ function consumeCodexRolloutLine(state, line, onEntry) {
|
|
|
677
729
|
state.rootCwd = stringOf(payload.cwd);
|
|
678
730
|
}
|
|
679
731
|
}
|
|
680
|
-
if (state.
|
|
732
|
+
if (state.hasInheritedHistory &&
|
|
681
733
|
!state.rootTaskStarted &&
|
|
682
734
|
payload?.type === "task_started" &&
|
|
683
735
|
isRootSpecificTaskStart(payload.started_at, state.rootStartedAtMs)) {
|
|
@@ -688,6 +740,11 @@ function consumeCodexRolloutLine(state, line, onEntry) {
|
|
|
688
740
|
// prompts/files cannot become the child's focus. Restored checkpoint
|
|
689
741
|
// evidence predating the boundary is parent history too and resets with
|
|
690
742
|
// it (the raw session cwd deliberately survives, exactly as rootCwd).
|
|
743
|
+
//
|
|
744
|
+
// Replayed task_started events keep the PARENT's original started_at, so
|
|
745
|
+
// `isRootSpecificTaskStart` rejects them and the first accepted task is
|
|
746
|
+
// this session's own — which is why the boundary lands exactly at the end
|
|
747
|
+
// of the replay.
|
|
691
748
|
state.inheritedUsageBaseline = state.lastTotal;
|
|
692
749
|
state.lastTotal = undefined;
|
|
693
750
|
state.rootTaskStarted = true;
|
|
@@ -699,10 +756,11 @@ function consumeCodexRolloutLine(state, line, onEntry) {
|
|
|
699
756
|
state.toolCallCount = 0;
|
|
700
757
|
state.model = undefined;
|
|
701
758
|
state.lastTurn = undefined;
|
|
759
|
+
state.maxTurnPromptTokens = undefined;
|
|
702
760
|
state.lastRateLimits = undefined;
|
|
703
761
|
state.lastActivityAt = toIso(stringOf(entry.timestamp)) ?? state.startedAt;
|
|
704
762
|
}
|
|
705
|
-
if (payload?.type === "task_started" && (!state.
|
|
763
|
+
if (payload?.type === "task_started" && (!state.hasInheritedHistory || state.rootTaskStarted)) {
|
|
706
764
|
state.pendingTaskTurnId = stringOf(payload.turn_id);
|
|
707
765
|
state.completedTask = undefined;
|
|
708
766
|
}
|
|
@@ -757,6 +815,10 @@ function consumeCodexRolloutLine(state, line, onEntry) {
|
|
|
757
815
|
if (turn) {
|
|
758
816
|
state.lastTurn = turn;
|
|
759
817
|
state.lastActivityAt = eventTimestamp;
|
|
818
|
+
const turnPrompt = codexTurnPromptTokens(turn);
|
|
819
|
+
if (turnPrompt !== undefined) {
|
|
820
|
+
state.maxTurnPromptTokens = Math.max(state.maxTurnPromptTokens ?? 0, turnPrompt);
|
|
821
|
+
}
|
|
760
822
|
}
|
|
761
823
|
const rateLimits = parseCodexRateLimits(payload.rate_limits, eventTimestamp);
|
|
762
824
|
if (rateLimits) {
|
|
@@ -774,7 +836,7 @@ function finishCodexRolloutParse(state, onDiagnostic) {
|
|
|
774
836
|
if (state.malformedLines > 0) {
|
|
775
837
|
onDiagnostic?.({ code: "malformed_jsonl", count: state.malformedLines });
|
|
776
838
|
}
|
|
777
|
-
if (!state.lastTotal || state.
|
|
839
|
+
if (!state.lastTotal || state.hasInheritedHistory && !state.rootTaskStarted)
|
|
778
840
|
return [];
|
|
779
841
|
const parsedUsage = parseCodexCumulativeUsage(state.lastTotal, state.inheritedUsageBaseline);
|
|
780
842
|
const parsedTurn = state.lastTurn
|
|
@@ -834,6 +896,9 @@ function finishCodexRolloutParse(state, onDiagnostic) {
|
|
|
834
896
|
: {}),
|
|
835
897
|
usageScope: "session_cumulative",
|
|
836
898
|
usageSupport,
|
|
899
|
+
...(usageSupport === "complete" && state.maxTurnPromptTokens !== undefined
|
|
900
|
+
? { maxRequestPromptTokens: state.maxTurnPromptTokens }
|
|
901
|
+
: {}),
|
|
837
902
|
...(parsedUsage.reportedTotalTokens !== undefined
|
|
838
903
|
? { reportedTotalTokens: parsedUsage.reportedTotalTokens }
|
|
839
904
|
: {}),
|
|
@@ -1999,6 +2064,7 @@ function serializeCodexReducerState(state) {
|
|
|
1999
2064
|
...(state.rootStartedAtMs !== undefined ? { rootStartedAtMs: state.rootStartedAtMs } : {}),
|
|
2000
2065
|
rootTaskStarted: state.rootTaskStarted,
|
|
2001
2066
|
isSubagent: state.isSubagent,
|
|
2067
|
+
hasInheritedHistory: state.hasInheritedHistory,
|
|
2002
2068
|
...(state.parentSessionId !== undefined ? { parentSessionId: state.parentSessionId } : {}),
|
|
2003
2069
|
...(state.pendingTaskTurnId !== undefined ? { pendingTaskTurnId: state.pendingTaskTurnId } : {}),
|
|
2004
2070
|
...(state.completedTask !== undefined ? { completedTask: state.completedTask } : {}),
|
|
@@ -2006,6 +2072,9 @@ function serializeCodexReducerState(state) {
|
|
|
2006
2072
|
...(state.lastActivityAt !== undefined ? { lastActivityAt: state.lastActivityAt } : {}),
|
|
2007
2073
|
...(state.lastTotal !== undefined ? { lastTotal: state.lastTotal } : {}),
|
|
2008
2074
|
...(state.lastTurn !== undefined ? { lastTurn: state.lastTurn } : {}),
|
|
2075
|
+
...(state.maxTurnPromptTokens !== undefined
|
|
2076
|
+
? { maxTurnPromptTokens: state.maxTurnPromptTokens }
|
|
2077
|
+
: {}),
|
|
2009
2078
|
...(state.inheritedUsageBaseline !== undefined
|
|
2010
2079
|
? { inheritedUsageBaseline: state.inheritedUsageBaseline }
|
|
2011
2080
|
: {}),
|
|
@@ -2050,6 +2119,8 @@ function isPersistedCodexReducerState(value) {
|
|
|
2050
2119
|
return typeof value.rootSessionMetaSeen === "boolean" &&
|
|
2051
2120
|
typeof value.rootTaskStarted === "boolean" &&
|
|
2052
2121
|
typeof value.isSubagent === "boolean" &&
|
|
2122
|
+
(value.hasInheritedHistory === undefined ||
|
|
2123
|
+
typeof value.hasInheritedHistory === "boolean") &&
|
|
2053
2124
|
optionalString(value.model) &&
|
|
2054
2125
|
optionalString(value.sessionId) &&
|
|
2055
2126
|
optionalString(value.sourceVersion) &&
|
|
@@ -2062,6 +2133,7 @@ function isPersistedCodexReducerState(value) {
|
|
|
2062
2133
|
optionalString(value.lastActivityAt) &&
|
|
2063
2134
|
optionalRecord(value.lastTotal) &&
|
|
2064
2135
|
optionalRecord(value.lastTurn) &&
|
|
2136
|
+
optionalFinite(value.maxTurnPromptTokens) &&
|
|
2065
2137
|
optionalRecord(value.inheritedUsageBaseline) &&
|
|
2066
2138
|
validRateLimits &&
|
|
2067
2139
|
validRootCwd &&
|
|
@@ -2084,6 +2156,10 @@ function restoreCodexReducerState(persisted) {
|
|
|
2084
2156
|
state.rootStartedAtMs = persisted.rootStartedAtMs;
|
|
2085
2157
|
state.rootTaskStarted = persisted.rootTaskStarted;
|
|
2086
2158
|
state.isSubagent = persisted.isSubagent;
|
|
2159
|
+
// Pre-fork-fix checkpoints carry no flag; fall back to the subagent bit so a
|
|
2160
|
+
// restored subagent keeps its boundary (a restored user fork re-parses, its
|
|
2161
|
+
// parser version having changed).
|
|
2162
|
+
state.hasInheritedHistory = persisted.hasInheritedHistory ?? persisted.isSubagent;
|
|
2087
2163
|
state.parentSessionId = persisted.parentSessionId;
|
|
2088
2164
|
state.pendingTaskTurnId = persisted.pendingTaskTurnId;
|
|
2089
2165
|
state.completedTask = persisted.completedTask;
|
|
@@ -2091,6 +2167,7 @@ function restoreCodexReducerState(persisted) {
|
|
|
2091
2167
|
state.lastActivityAt = persisted.lastActivityAt;
|
|
2092
2168
|
state.lastTotal = persisted.lastTotal;
|
|
2093
2169
|
state.lastTurn = persisted.lastTurn;
|
|
2170
|
+
state.maxTurnPromptTokens = persisted.maxTurnPromptTokens;
|
|
2094
2171
|
state.inheritedUsageBaseline = persisted.inheritedUsageBaseline;
|
|
2095
2172
|
state.lastRateLimits = persisted.lastRateLimits;
|
|
2096
2173
|
state.restoredRootCwd = persisted.rootCwd;
|
|
@@ -2726,8 +2803,11 @@ async function readCodexFinancialFile(file) {
|
|
|
2726
2803
|
const rootPayload = rootEntry?.type === "session_meta" && isRecord(rootEntry.payload)
|
|
2727
2804
|
? rootEntry.payload
|
|
2728
2805
|
: undefined;
|
|
2729
|
-
|
|
2730
|
-
|
|
2806
|
+
// A rollout that replays another session's history must be read WHOLE:
|
|
2807
|
+
// its inherited-baseline boundary sits mid-file, and a proof-complete tail
|
|
2808
|
+
// would never reach it. User forks need this exactly as subagents do.
|
|
2809
|
+
const hasInheritedHistory = rootPayload !== undefined &&
|
|
2810
|
+
codexHasInheritedHistory(rootPayload);
|
|
2731
2811
|
const entriesReverse = [];
|
|
2732
2812
|
let malformedLines = 0;
|
|
2733
2813
|
let prefilteredLines = 0;
|
|
@@ -2738,7 +2818,7 @@ async function readCodexFinancialFile(file) {
|
|
|
2738
2818
|
let suffixPartsReverse = [];
|
|
2739
2819
|
let stoppedEarly = false;
|
|
2740
2820
|
let bytesSkipped = 0;
|
|
2741
|
-
const proof = createCodexReverseProof(
|
|
2821
|
+
const proof = createCodexReverseProof(hasInheritedHistory, !rootPayload);
|
|
2742
2822
|
while (position > 0 && !stoppedEarly) {
|
|
2743
2823
|
const chunkStart = Math.max(0, position - FINANCIAL_REVERSE_CHUNK_BYTES);
|
|
2744
2824
|
const length = position - chunkStart;
|
|
@@ -2979,9 +3059,9 @@ function skipJsonWhitespace(input, start) {
|
|
|
2979
3059
|
index += 1;
|
|
2980
3060
|
return index;
|
|
2981
3061
|
}
|
|
2982
|
-
function createCodexReverseProof(
|
|
3062
|
+
function createCodexReverseProof(hasInheritedHistory, forceFullScan) {
|
|
2983
3063
|
return {
|
|
2984
|
-
|
|
3064
|
+
hasInheritedHistory,
|
|
2985
3065
|
forceFullScan,
|
|
2986
3066
|
totalSeen: false,
|
|
2987
3067
|
turnSeen: false,
|
|
@@ -2990,7 +3070,7 @@ function createCodexReverseProof(isSubagent, forceFullScan) {
|
|
|
2990
3070
|
};
|
|
2991
3071
|
}
|
|
2992
3072
|
function observeCodexReverseProof(proof, entry) {
|
|
2993
|
-
if (proof.forceFullScan || proof.
|
|
3073
|
+
if (proof.forceFullScan || proof.hasInheritedHistory)
|
|
2994
3074
|
return false;
|
|
2995
3075
|
const payload = isRecord(entry.payload) ? entry.payload : undefined;
|
|
2996
3076
|
const info = payload?.type === "token_count" && isRecord(payload.info)
|
|
@@ -3007,7 +3087,7 @@ function observeCodexReverseProof(proof, entry) {
|
|
|
3007
3087
|
if (entry.type === "turn_context" && stringOf(payload?.model)) {
|
|
3008
3088
|
proof.modelSeen = true;
|
|
3009
3089
|
}
|
|
3010
|
-
return !proof.
|
|
3090
|
+
return !proof.hasInheritedHistory &&
|
|
3011
3091
|
proof.totalSeen &&
|
|
3012
3092
|
proof.turnSeen &&
|
|
3013
3093
|
proof.rateLimitsSeen &&
|
|
@@ -3112,7 +3192,8 @@ function createCodexFinancialStreamState() {
|
|
|
3112
3192
|
return {
|
|
3113
3193
|
rootSessionMetaSeen: false,
|
|
3114
3194
|
rootTaskStarted: false,
|
|
3115
|
-
isSubagent: false
|
|
3195
|
+
isSubagent: false,
|
|
3196
|
+
hasInheritedHistory: false
|
|
3116
3197
|
};
|
|
3117
3198
|
}
|
|
3118
3199
|
function consumeCodexFinancialEntry(state, entry) {
|
|
@@ -3126,12 +3207,13 @@ function consumeCodexFinancialEntry(state, entry) {
|
|
|
3126
3207
|
state.rootStartedAtMs = timestampMilliseconds(payload.timestamp ?? entry.timestamp);
|
|
3127
3208
|
state.isSubagent = stringOf(payload.thread_source) === "subagent" ||
|
|
3128
3209
|
isRecord(payload.source) && "subagent" in payload.source;
|
|
3210
|
+
state.hasInheritedHistory = codexHasInheritedHistory(payload);
|
|
3129
3211
|
}
|
|
3130
3212
|
if (entry.type === "turn_context" && payload) {
|
|
3131
3213
|
state.model = stringOf(payload.model) ?? state.model;
|
|
3132
3214
|
state.rootCwd ??= stringOf(payload.cwd);
|
|
3133
3215
|
}
|
|
3134
|
-
if (state.
|
|
3216
|
+
if (state.hasInheritedHistory &&
|
|
3135
3217
|
!state.rootTaskStarted &&
|
|
3136
3218
|
payload?.type === "task_started" &&
|
|
3137
3219
|
isRootSpecificTaskStart(payload.started_at, state.rootStartedAtMs)) {
|
|
@@ -3140,6 +3222,7 @@ function consumeCodexFinancialEntry(state, entry) {
|
|
|
3140
3222
|
state.rootTaskStarted = true;
|
|
3141
3223
|
state.model = undefined;
|
|
3142
3224
|
state.lastTurn = undefined;
|
|
3225
|
+
state.maxTurnPromptTokens = undefined;
|
|
3143
3226
|
state.lastRateLimits = undefined;
|
|
3144
3227
|
state.lastActivityAt = toIso(stringOf(entry.timestamp)) ?? state.startedAt;
|
|
3145
3228
|
}
|
|
@@ -3162,13 +3245,17 @@ function consumeCodexFinancialEntry(state, entry) {
|
|
|
3162
3245
|
if (turn) {
|
|
3163
3246
|
state.lastTurn = turn;
|
|
3164
3247
|
state.lastActivityAt = eventTimestamp;
|
|
3248
|
+
const turnPrompt = codexTurnPromptTokens(turn);
|
|
3249
|
+
if (turnPrompt !== undefined) {
|
|
3250
|
+
state.maxTurnPromptTokens = Math.max(state.maxTurnPromptTokens ?? 0, turnPrompt);
|
|
3251
|
+
}
|
|
3165
3252
|
}
|
|
3166
3253
|
const rateLimits = parseCodexRateLimits(payload.rate_limits, eventTimestamp);
|
|
3167
3254
|
if (rateLimits)
|
|
3168
3255
|
state.lastRateLimits = rateLimits;
|
|
3169
3256
|
}
|
|
3170
3257
|
function finishCodexFinancialStream(state, onDiagnostic) {
|
|
3171
|
-
if (!state.lastTotal || state.
|
|
3258
|
+
if (!state.lastTotal || state.hasInheritedHistory && !state.rootTaskStarted)
|
|
3172
3259
|
return undefined;
|
|
3173
3260
|
const parsedUsage = parseCodexCumulativeUsage(state.lastTotal, state.inheritedUsageBaseline);
|
|
3174
3261
|
const parsedTurn = state.lastTurn
|
|
@@ -3196,6 +3283,9 @@ function finishCodexFinancialStream(state, onDiagnostic) {
|
|
|
3196
3283
|
: {}),
|
|
3197
3284
|
usageScope: "session_cumulative",
|
|
3198
3285
|
usageSupport,
|
|
3286
|
+
...(usageSupport === "complete" && state.maxTurnPromptTokens !== undefined
|
|
3287
|
+
? { maxRequestPromptTokens: state.maxTurnPromptTokens }
|
|
3288
|
+
: {}),
|
|
3199
3289
|
...(parsedUsage.reportedTotalTokens !== undefined
|
|
3200
3290
|
? { reportedTotalTokens: parsedUsage.reportedTotalTokens }
|
|
3201
3291
|
: {}),
|
|
@@ -3234,10 +3324,14 @@ export function aggregateCallsForFormats(calls, descriptors) {
|
|
|
3234
3324
|
const usageSupported = groupCalls.every((call) => call.usageSupport !== "unsupported_token_shape");
|
|
3235
3325
|
const sourceVersions = [...new Set(groupCalls.flatMap((call) => call.sourceVersion ? [call.sourceVersion] : []))].sort().slice(0, 8);
|
|
3236
3326
|
const tieredPricingEvidenceSupported = !usesPromptTieredPricing(model) ||
|
|
3237
|
-
groupCalls.every((call) => canPriceTokenUsageAtScope(model, call.usage, call.usageScope === "turn" ? "request" : "aggregate") && (agent !== "gemini-cli" || hasCompleteGeminiPromptEvidence(call)));
|
|
3327
|
+
groupCalls.every((call) => canPriceTokenUsageAtScope(model, call.usage, call.usageScope === "turn" ? "request" : "aggregate", call.maxRequestPromptTokens) && (agent !== "gemini-cli" || hasCompleteGeminiPromptEvidence(call)));
|
|
3238
3328
|
const amountUsd = usageSupported && tieredPricingEvidenceSupported && format
|
|
3239
3329
|
? usesPromptTieredPricing(model)
|
|
3240
|
-
? estimateTokenCostsUsd(model, groupCalls.map((call) => call.usage)
|
|
3330
|
+
? estimateTokenCostsUsd(model, groupCalls.map((call) => call.usage),
|
|
3331
|
+
// Fix each cumulative slice's tier from its largest single request
|
|
3332
|
+
// rather than its cache-inflated sum. Turn-scoped slices carry no
|
|
3333
|
+
// such evidence and keep selecting their tier from their own prompt.
|
|
3334
|
+
groupCalls.map((call) => call.maxRequestPromptTokens))
|
|
3241
3335
|
: estimateTokenCostUsd(model, usage)
|
|
3242
3336
|
: undefined;
|
|
3243
3337
|
const priced = usageSupported && typeof amountUsd === "number";
|
package/dist/modelPricing.d.ts
CHANGED
|
@@ -50,8 +50,17 @@ export declare function estimateTokenCostUsd(model: string, usage: TokenUsage):
|
|
|
50
50
|
* models whose entire request moves to a higher rate above a prompt-size
|
|
51
51
|
* threshold; pricing a daily token sum would incorrectly treat many small
|
|
52
52
|
* requests as one large request.
|
|
53
|
+
*
|
|
54
|
+
* `tierPromptTokens[i]`, when provided, fixes the tier of `usages[i]` from
|
|
55
|
+
* request-level evidence instead of the slice's own prompt total. A
|
|
56
|
+
* session-cumulative slice is a sum of many requests whose prompt total
|
|
57
|
+
* routinely clears a per-request threshold on cache reads alone, even though no
|
|
58
|
+
* single request did; supplying the largest single request's prompt keeps such
|
|
59
|
+
* a slice on the base tier (and pricing it there is exact, since the base rate
|
|
60
|
+
* distributes over the sum). Omitting the array preserves single-request
|
|
61
|
+
* behaviour: each slice's own prompt selects its tier.
|
|
53
62
|
*/
|
|
54
|
-
export declare function estimateTokenCostsUsd(model: string, usages: readonly TokenUsage[]): number | undefined;
|
|
63
|
+
export declare function estimateTokenCostsUsd(model: string, usages: readonly TokenUsage[], tierPromptTokens?: readonly (number | undefined)[]): number | undefined;
|
|
55
64
|
/** Whether this model's rate selection depends on each request's prompt size. */
|
|
56
65
|
export declare function usesPromptTieredPricing(model: string): boolean;
|
|
57
66
|
/** Prompt-size threshold for tiered request pricing, when one is published. */
|
|
@@ -60,9 +69,13 @@ export declare function promptTierThreshold(model: string): number | undefined;
|
|
|
60
69
|
* Tiered prices are selected per request, never from a multi-request sum.
|
|
61
70
|
* An aggregate is still unambiguous when its entire non-negative prompt-side
|
|
62
71
|
* total is at or below the threshold; then no constituent request can have
|
|
63
|
-
* crossed it.
|
|
64
|
-
*
|
|
72
|
+
* crossed it. It is also unambiguous when request-level evidence
|
|
73
|
+
* (`maxRequestPromptTokens`, the largest single request the aggregate contains)
|
|
74
|
+
* proves that no constituent request crossed the threshold: every request was
|
|
75
|
+
* base-tier, so the whole sum is base-tier and prices exactly at the base rate.
|
|
76
|
+
* Larger aggregates without such evidence fail closed to keep an unpriceable
|
|
77
|
+
* total honestly "missing" rather than guessing a tier.
|
|
65
78
|
*/
|
|
66
|
-
export declare function canPriceTokenUsageAtScope(model: string, usage: TokenUsage, scope: "request" | "aggregate"): boolean;
|
|
79
|
+
export declare function canPriceTokenUsageAtScope(model: string, usage: TokenUsage, scope: "request" | "aggregate", maxRequestPromptTokens?: number): boolean;
|
|
67
80
|
export {};
|
|
68
81
|
//# sourceMappingURL=modelPricing.d.ts.map
|
package/dist/modelPricing.js
CHANGED
|
@@ -20,21 +20,38 @@ const pricingRules = [
|
|
|
20
20
|
{ match: /^claude-haiku-4/i, inputPerM: 1, outputPerM: 5 },
|
|
21
21
|
{ match: /^claude-3-7-sonnet|^claude-3-5-sonnet/i, inputPerM: 3, outputPerM: 15 },
|
|
22
22
|
{ match: /^claude-3-5-haiku/i, inputPerM: 0.8, outputPerM: 4 },
|
|
23
|
-
// OpenAI (newer and more specific families must precede the GPT-5 fallback)
|
|
23
|
+
// OpenAI (newer and more specific families must precede the GPT-5 fallback).
|
|
24
|
+
// Rates from developers.openai.com/api/docs/pricing cross-checked against each
|
|
25
|
+
// model's own doc page, both fetched 2026-08-25.
|
|
26
|
+
//
|
|
27
|
+
// GPT-5.6 ships exactly three API models — sol, terra, luna
|
|
28
|
+
// (developers.openai.com/api/docs/models, 2026-08-25). Each 5.6/5.5/5.4 rule
|
|
29
|
+
// below is END-ANCHORED on purpose: an undocumented or future sibling
|
|
30
|
+
// (gpt-5.6-cyber, gpt-5.5-pro, gpt-5.7-sol) must fall through to
|
|
31
|
+
// honest-unpriced rather than inherit a neighbour's rate. That is the 0.9.4
|
|
32
|
+
// `^kimi-k2` mistake one family up, and it is the expensive direction here —
|
|
33
|
+
// the pre-0.9.6 `^gpt-5.6(?:-sol)?$` rule carried GPT-5.5's numbers, so every
|
|
34
|
+
// gpt-5.6-sol record was overstated by 25% on input and 50% on output.
|
|
35
|
+
//
|
|
36
|
+
// Long context, published identically on all three 5.6 pages plus 5.5/5.4:
|
|
37
|
+
// "Prompts with >272K input tokens are priced at 2x input and 1.5x output for
|
|
38
|
+
// the full request." Cached input scales with the 2x input leg.
|
|
24
39
|
{
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
40
|
+
// developers.openai.com/api/docs/models/gpt-5.6-sol, 2026-08-25
|
|
41
|
+
match: /^gpt-5\.6-sol$/i,
|
|
42
|
+
inputPerM: 4,
|
|
43
|
+
outputPerM: 20,
|
|
44
|
+
cacheReadPerM: 0.4,
|
|
29
45
|
abovePromptTokens: {
|
|
30
46
|
threshold: 272_000,
|
|
31
|
-
inputPerM:
|
|
32
|
-
outputPerM:
|
|
33
|
-
cacheReadPerM:
|
|
47
|
+
inputPerM: 8,
|
|
48
|
+
outputPerM: 30,
|
|
49
|
+
cacheReadPerM: 0.8
|
|
34
50
|
}
|
|
35
51
|
},
|
|
36
52
|
{
|
|
37
|
-
|
|
53
|
+
// developers.openai.com/api/docs/models/gpt-5.6-terra, 2026-08-25
|
|
54
|
+
match: /^gpt-5\.6-terra$/i,
|
|
38
55
|
inputPerM: 2,
|
|
39
56
|
outputPerM: 12,
|
|
40
57
|
cacheReadPerM: 0.2,
|
|
@@ -46,7 +63,8 @@ const pricingRules = [
|
|
|
46
63
|
}
|
|
47
64
|
},
|
|
48
65
|
{
|
|
49
|
-
|
|
66
|
+
// developers.openai.com/api/docs/models/gpt-5.6-luna, 2026-08-25
|
|
67
|
+
match: /^gpt-5\.6-luna$/i,
|
|
50
68
|
inputPerM: 0.2,
|
|
51
69
|
outputPerM: 1.2,
|
|
52
70
|
cacheReadPerM: 0.02,
|
|
@@ -57,15 +75,50 @@ const pricingRules = [
|
|
|
57
75
|
cacheReadPerM: 0.04
|
|
58
76
|
}
|
|
59
77
|
},
|
|
60
|
-
{
|
|
78
|
+
{
|
|
79
|
+
// developers.openai.com/api/docs/models/gpt-5.5, 2026-08-25
|
|
80
|
+
match: /^gpt-5\.5$/i,
|
|
81
|
+
inputPerM: 5,
|
|
82
|
+
outputPerM: 30,
|
|
83
|
+
cacheReadPerM: 0.5,
|
|
84
|
+
abovePromptTokens: {
|
|
85
|
+
threshold: 272_000,
|
|
86
|
+
inputPerM: 10,
|
|
87
|
+
outputPerM: 45,
|
|
88
|
+
cacheReadPerM: 1
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
// gpt-5.5-codex bills at the 5.5 base rate but is absent from the published
|
|
92
|
+
// long-context list, so it deliberately carries no >272K tier.
|
|
93
|
+
{ match: /^gpt-5\.5-codex$/i, inputPerM: 5, outputPerM: 30, cacheReadPerM: 0.5 },
|
|
61
94
|
{ match: /^gpt-5\.4-mini/i, inputPerM: 0.75, outputPerM: 4.5, cacheReadPerM: 0.075 },
|
|
62
95
|
{ match: /^gpt-5\.4-nano/i, inputPerM: 0.2, outputPerM: 1.25, cacheReadPerM: 0.02 },
|
|
63
|
-
{
|
|
96
|
+
{
|
|
97
|
+
// developers.openai.com/api/docs/models/gpt-5.4, 2026-08-25
|
|
98
|
+
match: /^gpt-5\.4$/i,
|
|
99
|
+
inputPerM: 2.5,
|
|
100
|
+
outputPerM: 15,
|
|
101
|
+
cacheReadPerM: 0.25,
|
|
102
|
+
abovePromptTokens: {
|
|
103
|
+
threshold: 272_000,
|
|
104
|
+
inputPerM: 5,
|
|
105
|
+
outputPerM: 22.5,
|
|
106
|
+
cacheReadPerM: 0.5
|
|
107
|
+
}
|
|
108
|
+
},
|
|
64
109
|
{ match: /^gpt-5\.3-codex/i, inputPerM: 1.75, outputPerM: 14, cacheReadPerM: 0.175 },
|
|
65
110
|
{ match: /^gpt-5\.2(?:-codex)?/i, inputPerM: 1.75, outputPerM: 14, cacheReadPerM: 0.175 },
|
|
66
111
|
{ match: /^gpt-5(?:\.1)?-codex/i, inputPerM: 1.25, outputPerM: 10, cacheReadPerM: 0.125 },
|
|
67
112
|
{ match: /^gpt-5(?:\.1)?-mini/i, inputPerM: 0.25, outputPerM: 2, cacheReadPerM: 0.025 },
|
|
68
|
-
|
|
113
|
+
// developers.openai.com/api/docs/models/gpt-5-nano, 2026-08-25. Before 0.9.6
|
|
114
|
+
// this fell through to the ^gpt-5 fallback and billed at $1.25/$10 — 25x the
|
|
115
|
+
// real rate in both directions. No published long-context tier.
|
|
116
|
+
{ match: /^gpt-5-nano/i, inputPerM: 0.05, outputPerM: 0.4, cacheReadPerM: 0.005 },
|
|
117
|
+
// GPT-5 base and its dash-suffixed snapshots only. The `-|$` boundary stops
|
|
118
|
+
// this fallback from swallowing dot-minor families it knows nothing about:
|
|
119
|
+
// gpt-5.7-*, gpt-5.6-cyber and any future gpt-5.6-<variant> now return
|
|
120
|
+
// undefined -> "missing" instead of silently billing at GPT-5's $1.25/$10.
|
|
121
|
+
{ match: /^gpt-5(?:-|$)/i, inputPerM: 1.25, outputPerM: 10, cacheReadPerM: 0.125 },
|
|
69
122
|
{ match: /^gpt-4\.1-nano/i, inputPerM: 0.1, outputPerM: 0.4 },
|
|
70
123
|
{ match: /^gpt-4\.1-mini/i, inputPerM: 0.4, outputPerM: 1.6 },
|
|
71
124
|
{ match: /^gpt-4\.1/i, inputPerM: 2, outputPerM: 8, cacheReadPerM: 0.5 },
|
|
@@ -122,6 +175,23 @@ const pricingRules = [
|
|
|
122
175
|
// - deepseek-v4-* (api-docs.deepseek.com/quick_start/pricing): published
|
|
123
176
|
// rates are time-of-day (off-peak = half price, up to 2x swing), so any
|
|
124
177
|
// flat number here would be dishonest; needs timestamp-aware pricing.
|
|
178
|
+
//
|
|
179
|
+
// Deliberate deferrals (2026-08-25 OpenAI review), same honest path:
|
|
180
|
+
// - gpt-5.6-cyber / gpt-5.5-cyber ($12.50/$1.25/$75 on the pricing page):
|
|
181
|
+
// the two canonical sources disagree on whether the >272K tier applies —
|
|
182
|
+
// the pricing page omits cyber from its long-context list while
|
|
183
|
+
// developers.openai.com/api/docs/models/gpt-5.6-cyber states the 2x/1.5x
|
|
184
|
+
// rule does apply. Access is gated behind the Daybreak program, so the
|
|
185
|
+
// cost of leaving it unpriced is near zero and a coin-flip on the tier
|
|
186
|
+
// would be a real number that is wrong on long requests.
|
|
187
|
+
// - gpt-5.5-pro / gpt-5.4-pro: listed as long-context-capable but no
|
|
188
|
+
// per-model rate is published on either canonical source.
|
|
189
|
+
// - bare gpt-5.1 / gpt-5.3: the pricing page quotes the 5/5.1/5.2 group as a
|
|
190
|
+
// RANGE ($1.25-$1.75) and neither has a resolved per-model figure. Their
|
|
191
|
+
// -codex and -mini variants keep their own verified rules above.
|
|
192
|
+
// - gpt-5.6-codex / gpt-5.5-mini: NOT OpenAI model ids (both 404 on the model
|
|
193
|
+
// docs and are absent from developers.openai.com/api/docs/models). They
|
|
194
|
+
// appear only in this repo's fixtures and sample CSVs.
|
|
125
195
|
];
|
|
126
196
|
export function findPricingRule(model) {
|
|
127
197
|
return pricingRules.find((rule) => rule.match.test(model));
|
|
@@ -139,11 +209,20 @@ export function estimateTokenCostUsd(model, usage) {
|
|
|
139
209
|
* models whose entire request moves to a higher rate above a prompt-size
|
|
140
210
|
* threshold; pricing a daily token sum would incorrectly treat many small
|
|
141
211
|
* requests as one large request.
|
|
212
|
+
*
|
|
213
|
+
* `tierPromptTokens[i]`, when provided, fixes the tier of `usages[i]` from
|
|
214
|
+
* request-level evidence instead of the slice's own prompt total. A
|
|
215
|
+
* session-cumulative slice is a sum of many requests whose prompt total
|
|
216
|
+
* routinely clears a per-request threshold on cache reads alone, even though no
|
|
217
|
+
* single request did; supplying the largest single request's prompt keeps such
|
|
218
|
+
* a slice on the base tier (and pricing it there is exact, since the base rate
|
|
219
|
+
* distributes over the sum). Omitting the array preserves single-request
|
|
220
|
+
* behaviour: each slice's own prompt selects its tier.
|
|
142
221
|
*/
|
|
143
|
-
export function estimateTokenCostsUsd(model, usages) {
|
|
222
|
+
export function estimateTokenCostsUsd(model, usages, tierPromptTokens) {
|
|
144
223
|
let total = 0;
|
|
145
|
-
for (
|
|
146
|
-
const usd = rawTokenCostUsd(model,
|
|
224
|
+
for (let index = 0; index < usages.length; index += 1) {
|
|
225
|
+
const usd = rawTokenCostUsd(model, usages[index], tierPromptTokens?.[index]);
|
|
147
226
|
if (usd === undefined)
|
|
148
227
|
return undefined;
|
|
149
228
|
total += usd;
|
|
@@ -162,21 +241,34 @@ export function promptTierThreshold(model) {
|
|
|
162
241
|
* Tiered prices are selected per request, never from a multi-request sum.
|
|
163
242
|
* An aggregate is still unambiguous when its entire non-negative prompt-side
|
|
164
243
|
* total is at or below the threshold; then no constituent request can have
|
|
165
|
-
* crossed it.
|
|
166
|
-
*
|
|
244
|
+
* crossed it. It is also unambiguous when request-level evidence
|
|
245
|
+
* (`maxRequestPromptTokens`, the largest single request the aggregate contains)
|
|
246
|
+
* proves that no constituent request crossed the threshold: every request was
|
|
247
|
+
* base-tier, so the whole sum is base-tier and prices exactly at the base rate.
|
|
248
|
+
* Larger aggregates without such evidence fail closed to keep an unpriceable
|
|
249
|
+
* total honestly "missing" rather than guessing a tier.
|
|
167
250
|
*/
|
|
168
|
-
export function canPriceTokenUsageAtScope(model, usage, scope) {
|
|
251
|
+
export function canPriceTokenUsageAtScope(model, usage, scope, maxRequestPromptTokens) {
|
|
169
252
|
const threshold = promptTierThreshold(model);
|
|
170
253
|
if (threshold === undefined || scope === "request")
|
|
171
254
|
return true;
|
|
172
|
-
|
|
255
|
+
if (effectivePromptTokens(usage) <= threshold)
|
|
256
|
+
return true;
|
|
257
|
+
return maxRequestPromptTokens !== undefined && maxRequestPromptTokens <= threshold;
|
|
173
258
|
}
|
|
174
|
-
|
|
259
|
+
/**
|
|
260
|
+
* @param tierPromptTokens Prompt size used ONLY to select the request tier,
|
|
261
|
+
* when it differs from the priced slice's own prompt total (e.g. a
|
|
262
|
+
* session-cumulative slice whose tier is fixed by its largest single
|
|
263
|
+
* request). Component pricing always uses `usage`; defaults to the slice's
|
|
264
|
+
* own effective prompt so single-request callers are unchanged.
|
|
265
|
+
*/
|
|
266
|
+
function rawTokenCostUsd(model, usage, tierPromptTokens) {
|
|
175
267
|
const rule = findPricingRule(model);
|
|
176
268
|
if (!rule) {
|
|
177
269
|
return undefined;
|
|
178
270
|
}
|
|
179
|
-
const promptTokens = effectivePromptTokens(usage);
|
|
271
|
+
const promptTokens = tierPromptTokens ?? effectivePromptTokens(usage);
|
|
180
272
|
const rates = rule.abovePromptTokens &&
|
|
181
273
|
promptTokens > rule.abovePromptTokens.threshold
|
|
182
274
|
? rule.abovePromptTokens
|