tledger 0.3.1 → 0.4.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 +283 -157
- package/bin/token-ledger-cache-data.mjs +492 -0
- package/bin/token-ledger-cache-image.mjs +7 -1147
- package/bin/token-ledger-cache-sections.mjs +848 -0
- package/bin/token-ledger-cost-terminal.mjs +234 -0
- package/bin/token-ledger-image-layout.mjs +20 -0
- package/bin/token-ledger-image-primitives.mjs +192 -0
- package/bin/token-ledger-report-data.mjs +1159 -0
- package/bin/token-ledger-source-status.mjs +31 -0
- package/bin/token-ledger-terminal.mjs +245 -69
- package/bin/token-ledger-trend-image.mjs +2025 -1724
- package/bin/token-ledger-trend-terminal.mjs +273 -150
- package/bin/token-ledger-trend.mjs +204 -198
- package/bin/token-ledger-tui.mjs +180 -51
- package/bin/token-ledger.mjs +735 -208
- package/docs/durable-ledger-operations.md +198 -0
- package/docs/release-notes-0.4.0.md +41 -0
- package/docs/token-ledger-report-7-day.png +0 -0
- package/lib/token-ledger-calendar.mjs +225 -0
- package/lib/token-ledger-collection.mjs +100 -0
- package/lib/token-ledger-importer.mjs +3329 -488
- package/lib/token-ledger-labels.mjs +66 -0
- package/lib/token-ledger-ledger.mjs +6056 -0
- package/lib/token-ledger-quota-contract.mjs +38 -0
- package/lib/token-ledger-range-analysis.mjs +120 -0
- package/lib/token-ledger-rates.mjs +330 -0
- package/lib/token-ledger-snapshot.mjs +336 -35
- package/lib/token-ledger-terminal-text.mjs +11 -0
- package/lib/token-ledger-usage.mjs +339 -33
- package/package.json +13 -10
- package/bin/token-ledger-rates.mjs +0 -65
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export const QUOTA_IDENTITY_CONTRACT_VERSION = "codex-limit-id-v2";
|
|
4
|
+
export const ACCOUNT_QUOTA_LIMIT_KEY = createHash("sha256")
|
|
5
|
+
.update("codex")
|
|
6
|
+
.digest("hex")
|
|
7
|
+
.slice(0, 16);
|
|
8
|
+
|
|
9
|
+
function primitiveString(value) {
|
|
10
|
+
try {
|
|
11
|
+
const text = String.prototype.valueOf.call(value);
|
|
12
|
+
return text === value ? text : null;
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function snapshotQuotaIdentityContract(snapshot = {}) {
|
|
19
|
+
return primitiveString(
|
|
20
|
+
snapshot?.metadata?.durableLedger?.quotaIdentityContract,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function snapshotHasCurrentQuotaIdentityContract(snapshot = {}) {
|
|
25
|
+
return snapshotQuotaIdentityContract(snapshot) ===
|
|
26
|
+
QUOTA_IDENTITY_CONTRACT_VERSION;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function quotaIdentityMatchesContract({ limitKey, scope } = {}) {
|
|
30
|
+
const key = primitiveString(limitKey);
|
|
31
|
+
if (
|
|
32
|
+
key === null ||
|
|
33
|
+
!/^[0-9a-f]{16}$/i.test(key) ||
|
|
34
|
+
(scope !== "account" && scope !== "named")
|
|
35
|
+
) return false;
|
|
36
|
+
return (key.toLowerCase() === ACCOUNT_QUOTA_LIMIT_KEY) ===
|
|
37
|
+
(scope === "account");
|
|
38
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createTimeZoneFormatter,
|
|
3
|
+
localDateBoundary,
|
|
4
|
+
shiftCalendarDate,
|
|
5
|
+
} from "./token-ledger-calendar.mjs";
|
|
6
|
+
import {
|
|
7
|
+
splitUsageBucketsAtBoundaries,
|
|
8
|
+
usageBuckets,
|
|
9
|
+
} from "./token-ledger-usage.mjs";
|
|
10
|
+
|
|
11
|
+
function finiteTimestamp(value) {
|
|
12
|
+
try {
|
|
13
|
+
const timestamp = new Date(value).getTime();
|
|
14
|
+
return Number.isFinite(timestamp) ? timestamp : null;
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function rangeEvents(events, bounds) {
|
|
21
|
+
const startMs = bounds.start.getTime();
|
|
22
|
+
const endMs = bounds.end.getTime();
|
|
23
|
+
return events.filter((event) => {
|
|
24
|
+
const timestampMs = finiteTimestamp(event?.timestamp);
|
|
25
|
+
return timestampMs !== null && timestampMs >= startMs && timestampMs < endMs;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function eventsThrough(events, endMs) {
|
|
30
|
+
return events.filter((event) => {
|
|
31
|
+
const timestampMs = finiteTimestamp(event?.timestamp);
|
|
32
|
+
return timestampMs !== null && timestampMs < endMs;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function freezeEvents(events) {
|
|
37
|
+
return Object.freeze([...events]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function localCalendarBoundaries(bounds) {
|
|
41
|
+
if (
|
|
42
|
+
!bounds?.timeZone ||
|
|
43
|
+
!bounds?.startDateString ||
|
|
44
|
+
!bounds?.endDateString
|
|
45
|
+
) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
const formatter = createTimeZoneFormatter(bounds.timeZone);
|
|
49
|
+
const finalDateString = shiftCalendarDate(bounds.endDateString, 1);
|
|
50
|
+
const boundaries = [];
|
|
51
|
+
for (
|
|
52
|
+
let dateString = bounds.startDateString;
|
|
53
|
+
dateString <= finalDateString;
|
|
54
|
+
dateString = shiftCalendarDate(dateString, 1)
|
|
55
|
+
) {
|
|
56
|
+
boundaries.push(
|
|
57
|
+
localDateBoundary(dateString, bounds.timeZone, formatter).getTime(),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return boundaries;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Calendar totals and meter attribution use different boundaries. A quota
|
|
64
|
+
// sample can require allocating a bucket between meter intervals without
|
|
65
|
+
// making that bucket's already-known daily token total an estimate.
|
|
66
|
+
export function buildRangeAnalysis(
|
|
67
|
+
snapshot = {},
|
|
68
|
+
bounds,
|
|
69
|
+
{ priorBounds = null, quotaObservations = [] } = {},
|
|
70
|
+
) {
|
|
71
|
+
const endMs = bounds.end.getTime();
|
|
72
|
+
const scopedQuotaObservations = quotaObservations.filter(
|
|
73
|
+
(observation) =>
|
|
74
|
+
Number.isFinite(observation?.timestampMs) &&
|
|
75
|
+
observation.timestampMs < endMs,
|
|
76
|
+
);
|
|
77
|
+
const boundaryValues = [
|
|
78
|
+
bounds.start.getTime(),
|
|
79
|
+
bounds.end.getTime(),
|
|
80
|
+
...localCalendarBoundaries(bounds),
|
|
81
|
+
...(priorBounds
|
|
82
|
+
? [
|
|
83
|
+
priorBounds.start.getTime(),
|
|
84
|
+
priorBounds.end.getTime(),
|
|
85
|
+
...localCalendarBoundaries(priorBounds),
|
|
86
|
+
]
|
|
87
|
+
: []),
|
|
88
|
+
];
|
|
89
|
+
const sourceEvents = usageBuckets(snapshot);
|
|
90
|
+
const splitEvents = splitUsageBucketsAtBoundaries(
|
|
91
|
+
sourceEvents,
|
|
92
|
+
boundaryValues,
|
|
93
|
+
);
|
|
94
|
+
const currentEvents = rangeEvents(splitEvents, bounds);
|
|
95
|
+
// null marks a prior range that was never requested; renderers treat an
|
|
96
|
+
// array as authoritative, so an empty one would hide real prior events.
|
|
97
|
+
const priorEvents = priorBounds === null
|
|
98
|
+
? null
|
|
99
|
+
: rangeEvents(splitEvents, priorBounds);
|
|
100
|
+
const quotaBoundaries = scopedQuotaObservations.flatMap((observation) => [
|
|
101
|
+
observation.cycleStartMs,
|
|
102
|
+
observation.timestampMs,
|
|
103
|
+
]);
|
|
104
|
+
const trendEvents = splitUsageBucketsAtBoundaries(
|
|
105
|
+
eventsThrough(splitEvents, endMs),
|
|
106
|
+
quotaBoundaries,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
return Object.freeze({
|
|
110
|
+
allEvents: freezeEvents(splitEvents),
|
|
111
|
+
currentEvents: freezeEvents(currentEvents),
|
|
112
|
+
priorEvents: priorEvents === null ? null : freezeEvents(priorEvents),
|
|
113
|
+
trendEvents: freezeEvents(trendEvents),
|
|
114
|
+
quotaObservations: Object.freeze([...scopedQuotaObservations]),
|
|
115
|
+
sourceBucketCount: sourceEvents.length,
|
|
116
|
+
boundaryCount: new Set(
|
|
117
|
+
boundaryValues.map(Number).filter(Number.isFinite),
|
|
118
|
+
).size,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
// Purchased-credit weights are a separate attribution lens. They never scale
|
|
2
|
+
// raw token totals or replace OpenAI-reported plan-limit meter readings.
|
|
3
|
+
import {
|
|
4
|
+
isValidTokenValue,
|
|
5
|
+
tokenTotalsReconcile,
|
|
6
|
+
} from "../lib/token-ledger-usage.mjs";
|
|
7
|
+
|
|
8
|
+
export const CODEX_CREDIT_RATE_CARD_AS_OF = "2026-08-23";
|
|
9
|
+
export const CODEX_CREDIT_RATE_CARD_URL =
|
|
10
|
+
"https://help.openai.com/en/articles/11481834";
|
|
11
|
+
export const CODEX_CREDIT_RATE_CARD_KIND = "codex-purchased-credits";
|
|
12
|
+
export const CODEX_CREDIT_RATE_CARD_SCOPE =
|
|
13
|
+
"Estimate for eligible Codex usage paid with purchased credits; not API USD and not included plan-limit meter usage.";
|
|
14
|
+
|
|
15
|
+
export const CODEX_CREDIT_RATE_CARD = Object.freeze({
|
|
16
|
+
"gpt-5.6-sol": Object.freeze({ input: 100, cached: 10, output: 500 }),
|
|
17
|
+
"gpt-5.6-terra": Object.freeze({ input: 50, cached: 5, output: 300 }),
|
|
18
|
+
"gpt-5.6-luna": Object.freeze({ input: 5, cached: 0.5, output: 30 }),
|
|
19
|
+
"gpt-5.5": Object.freeze({ input: 125, cached: 12.5, output: 750 }),
|
|
20
|
+
"daybreak-blue": Object.freeze({ input: 100, cached: 10, output: 500 }),
|
|
21
|
+
"daybreak-red": Object.freeze({ input: 312.5, cached: 31.25, output: 1_875 }),
|
|
22
|
+
"gpt-5.4": Object.freeze({ input: 62.5, cached: 6.25, output: 375 }),
|
|
23
|
+
"gpt-5.4-mini": Object.freeze({ input: 18.75, cached: 1.875, output: 113 }),
|
|
24
|
+
"gpt-5.3-codex": Object.freeze({ input: 43.75, cached: 4.375, output: 350 }),
|
|
25
|
+
"gpt-5.2": Object.freeze({ input: 43.75, cached: 4.375, output: 350 }),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// API-equivalent USD is a separate hypothetical lens. These values are never
|
|
29
|
+
// derived from purchased credits and never claim to reproduce an invoice.
|
|
30
|
+
export const API_USD_RATE_CARD_AS_OF = "2026-08-23";
|
|
31
|
+
export const API_USD_RATE_CARD_URL =
|
|
32
|
+
"https://help.openai.com/en/articles/20001415";
|
|
33
|
+
export const API_USD_SOL_MODEL_URL =
|
|
34
|
+
"https://developers.openai.com/api/docs/models/gpt-5.6-sol";
|
|
35
|
+
export const API_USD_FAST_MODE_URL =
|
|
36
|
+
"https://developers.openai.com/api/docs/guides/fast-mode";
|
|
37
|
+
export const API_USD_RATE_CARD = Object.freeze({
|
|
38
|
+
"gpt-5.6-sol": Object.freeze({ input: 4, cached: 0.4, output: 20, cacheWrite: 1.25 }),
|
|
39
|
+
"gpt-5.6-terra": Object.freeze({ input: 2, cached: 0.2, output: 12, cacheWrite: 1.25 }),
|
|
40
|
+
"gpt-5.6-luna": Object.freeze({ input: 0.2, cached: 0.02, output: 1.2, cacheWrite: 1.25 }),
|
|
41
|
+
"gpt-5.5": Object.freeze({ input: 5, cached: 0.5, output: 30 }),
|
|
42
|
+
"daybreak-blue": Object.freeze({ input: 4, cached: 0.4, output: 20, cacheWrite: 1.25 }),
|
|
43
|
+
"daybreak-red": Object.freeze({ input: 12.5, cached: 1.25, output: 75, cacheWrite: 1.25 }),
|
|
44
|
+
"gpt-5.4": Object.freeze({ input: 2.5, cached: 0.25, output: 15 }),
|
|
45
|
+
"gpt-5.4-mini": Object.freeze({ input: 0.75, cached: 0.075, output: 4.5 }),
|
|
46
|
+
"gpt-5.3-codex": Object.freeze({ input: 1.75, cached: 0.175, output: 14 }),
|
|
47
|
+
"gpt-5.2": Object.freeze({ input: 1.75, cached: 0.175, output: 14 }),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export const API_USD_LONG_CONTEXT_THRESHOLD_TOKENS = 272_000;
|
|
51
|
+
|
|
52
|
+
// These are exact identifiers observed in current Codex metadata. Keep this
|
|
53
|
+
// list explicit so a future model name cannot silently inherit an old price.
|
|
54
|
+
const MODEL_ALIASES = new Map([
|
|
55
|
+
["gpt-5.5-cyber", "daybreak-red"],
|
|
56
|
+
["gpt-5.5-cyber-preview", "daybreak-red"],
|
|
57
|
+
["gpt-5.6-cyber", "daybreak-red"],
|
|
58
|
+
["gpt-daybreak-red", "daybreak-red"],
|
|
59
|
+
["gpt-5.5-daybreak-red-latest", "daybreak-red"],
|
|
60
|
+
["gpt-daybreak-red-latest", "daybreak-red"],
|
|
61
|
+
["gpt-daybreak-blue", "daybreak-blue"],
|
|
62
|
+
["gpt-5.5-daybreak-blue-latest", "daybreak-blue"],
|
|
63
|
+
["gpt-daybreak-blue-latest", "daybreak-blue"],
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
// Fast multipliers stay keyed to exact identifiers whose generation is
|
|
67
|
+
// explicit in the identifier or documented by the current Daybreak FAQ.
|
|
68
|
+
const FAST_MULTIPLIER_BY_IDENTIFIER = new Map([
|
|
69
|
+
["gpt-5.6-sol", 2.5],
|
|
70
|
+
["gpt-5.6-terra", 2.5],
|
|
71
|
+
["gpt-5.6-luna", 2.5],
|
|
72
|
+
["gpt-5.6-cyber", 2.5],
|
|
73
|
+
["daybreak-blue", 2.5],
|
|
74
|
+
["daybreak-red", 2.5],
|
|
75
|
+
["gpt-daybreak-blue", 2.5],
|
|
76
|
+
["gpt-daybreak-red", 2.5],
|
|
77
|
+
["gpt-5.5", 2.5],
|
|
78
|
+
["gpt-5.5-cyber", 2.5],
|
|
79
|
+
["gpt-5.5-cyber-preview", 2.5],
|
|
80
|
+
["gpt-5.4", 2],
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
function normalizedIdentifier(value) {
|
|
84
|
+
return String(value ?? "")
|
|
85
|
+
.trim()
|
|
86
|
+
.toLowerCase()
|
|
87
|
+
.replace(/[\s_]+/g, "-");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function normalizeCodexCreditModel(model) {
|
|
91
|
+
const value = normalizedIdentifier(model);
|
|
92
|
+
if (Object.hasOwn(CODEX_CREDIT_RATE_CARD, value)) return value;
|
|
93
|
+
return MODEL_ALIASES.get(value) ?? (value || "unknown");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function isFastServiceTier(serviceTier) {
|
|
97
|
+
const tier = normalizedIdentifier(serviceTier);
|
|
98
|
+
return tier === "priority" || tier === "fast";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function codexCreditMultiplier(model, serviceTier) {
|
|
102
|
+
if (!isFastServiceTier(serviceTier)) return 1;
|
|
103
|
+
return FAST_MULTIPLIER_BY_IDENTIFIER.get(normalizedIdentifier(model)) ?? null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function nonNegativeFinite(value, fallback = null) {
|
|
107
|
+
if (value === null || value === undefined) return fallback;
|
|
108
|
+
const number = Number(value);
|
|
109
|
+
return Number.isFinite(number) ? Math.max(0, number) : fallback;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function hasDetailedTokenBreakdown(usage) {
|
|
113
|
+
if (usage === null || usage === undefined) return false;
|
|
114
|
+
const allowFractional = usage.rangeAllocationEstimated === true;
|
|
115
|
+
const totalTokens = isValidTokenValue(usage.totalTokens, {
|
|
116
|
+
allowFractional,
|
|
117
|
+
})
|
|
118
|
+
? usage.totalTokens
|
|
119
|
+
: null;
|
|
120
|
+
if (totalTokens === null) return false;
|
|
121
|
+
if (totalTokens === 0) {
|
|
122
|
+
return usage.breakdownAvailable !== false && usage.componentsValid !== false;
|
|
123
|
+
}
|
|
124
|
+
const optionalToken = (value) =>
|
|
125
|
+
value === undefined
|
|
126
|
+
? 0
|
|
127
|
+
: isValidTokenValue(value, { allowFractional })
|
|
128
|
+
? value
|
|
129
|
+
: null;
|
|
130
|
+
const cachedToken = (value) =>
|
|
131
|
+
value === undefined
|
|
132
|
+
? 0
|
|
133
|
+
: isValidTokenValue(value, { allowFractional })
|
|
134
|
+
? value
|
|
135
|
+
: Number.isFinite(value) && value < 0
|
|
136
|
+
? 0
|
|
137
|
+
: null;
|
|
138
|
+
if (usage.inputTokens === undefined || usage.outputTokens === undefined) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
const inputTokens = optionalToken(usage.inputTokens);
|
|
142
|
+
const cachedInputTokens = cachedToken(usage.cachedInputTokens);
|
|
143
|
+
const cacheWriteInputTokens = optionalToken(usage.cacheWriteInputTokens);
|
|
144
|
+
const outputTokens = optionalToken(usage.outputTokens);
|
|
145
|
+
const reasoningTokens = optionalToken(usage.reasoningTokens);
|
|
146
|
+
if (
|
|
147
|
+
totalTokens === null ||
|
|
148
|
+
inputTokens === null ||
|
|
149
|
+
cachedInputTokens === null ||
|
|
150
|
+
cacheWriteInputTokens === null ||
|
|
151
|
+
outputTokens === null ||
|
|
152
|
+
reasoningTokens === null ||
|
|
153
|
+
usage.breakdownAvailable === false ||
|
|
154
|
+
usage.componentsValid === false
|
|
155
|
+
) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
return (
|
|
159
|
+
tokenTotalsReconcile(
|
|
160
|
+
totalTokens,
|
|
161
|
+
inputTokens,
|
|
162
|
+
outputTokens,
|
|
163
|
+
allowFractional,
|
|
164
|
+
) &&
|
|
165
|
+
(inputTokens > 0 || outputTokens > 0)
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function partitionTokenUsage(usage) {
|
|
170
|
+
if (!hasDetailedTokenBreakdown(usage)) return null;
|
|
171
|
+
const inputTokens = nonNegativeFinite(usage.inputTokens, 0);
|
|
172
|
+
const outputTokens = nonNegativeFinite(usage.outputTokens, 0);
|
|
173
|
+
const cachedInputTokens = Math.min(
|
|
174
|
+
inputTokens,
|
|
175
|
+
nonNegativeFinite(usage.cachedInputTokens, 0),
|
|
176
|
+
);
|
|
177
|
+
const cacheWriteInputTokens = Math.min(
|
|
178
|
+
inputTokens - cachedInputTokens,
|
|
179
|
+
nonNegativeFinite(usage.cacheWriteInputTokens, 0),
|
|
180
|
+
);
|
|
181
|
+
const reasoningTokens = Math.min(
|
|
182
|
+
outputTokens,
|
|
183
|
+
nonNegativeFinite(usage.reasoningTokens, 0),
|
|
184
|
+
);
|
|
185
|
+
return {
|
|
186
|
+
uncachedInputTokens: inputTokens - cachedInputTokens - cacheWriteInputTokens,
|
|
187
|
+
cachedInputTokens,
|
|
188
|
+
cacheWriteInputTokens,
|
|
189
|
+
outputTokens,
|
|
190
|
+
reasoningTokens,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function calculateCodexPurchasedCredits({ model, serviceTier, usage }) {
|
|
195
|
+
const rate = CODEX_CREDIT_RATE_CARD[normalizeCodexCreditModel(model)];
|
|
196
|
+
const partition = partitionTokenUsage(usage);
|
|
197
|
+
const multiplier = codexCreditMultiplier(model, serviceTier);
|
|
198
|
+
if (!rate || !partition || multiplier === null) return null;
|
|
199
|
+
const baseCredits = (
|
|
200
|
+
(partition.uncachedInputTokens + partition.cacheWriteInputTokens) * rate.input +
|
|
201
|
+
partition.cachedInputTokens * rate.cached +
|
|
202
|
+
partition.outputTokens * rate.output
|
|
203
|
+
) / 1_000_000;
|
|
204
|
+
return baseCredits * multiplier;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function apiServiceTier(serviceTier) {
|
|
208
|
+
const tier = normalizedIdentifier(serviceTier);
|
|
209
|
+
if (!tier || tier === "default" || tier === "standard") return "standard";
|
|
210
|
+
if (tier === "fast" || tier === "priority") return "fast";
|
|
211
|
+
if (tier === "ultrafast") return "ultrafast";
|
|
212
|
+
return "unsupported";
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function apiCallCount(value, compacted) {
|
|
216
|
+
const number = Number(value);
|
|
217
|
+
if (Number.isFinite(number) && number > 0) return number;
|
|
218
|
+
return compacted ? null : 1;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function apiUnratedResult(usage, reason, estimated = false) {
|
|
222
|
+
return {
|
|
223
|
+
amount: null,
|
|
224
|
+
currency: "USD",
|
|
225
|
+
ratedTokens: 0,
|
|
226
|
+
unratedTokens: nonNegativeFinite(usage?.totalTokens, 0),
|
|
227
|
+
complete: false,
|
|
228
|
+
estimated,
|
|
229
|
+
reasons: [reason],
|
|
230
|
+
partition: null,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function apiUsdForUsage(event) {
|
|
235
|
+
const usage = event?.usage ?? event;
|
|
236
|
+
const model = event?.rateCardModel ?? usage?.rateCardModel ??
|
|
237
|
+
event?.model ?? usage?.model;
|
|
238
|
+
const serviceTier = event?.serviceTier ?? usage?.serviceTier;
|
|
239
|
+
const normalizedModel = normalizeCodexCreditModel(model);
|
|
240
|
+
const rate = API_USD_RATE_CARD[normalizedModel];
|
|
241
|
+
const partition = partitionTokenUsage(usage);
|
|
242
|
+
const resolutionSeconds = nonNegativeFinite(
|
|
243
|
+
event?.resolutionSeconds ?? usage?.resolutionSeconds,
|
|
244
|
+
0,
|
|
245
|
+
);
|
|
246
|
+
const compacted = Boolean(
|
|
247
|
+
event?.rangeAllocationEstimated === true ||
|
|
248
|
+
usage?.rangeAllocationEstimated === true ||
|
|
249
|
+
resolutionSeconds > 0
|
|
250
|
+
);
|
|
251
|
+
const callCount = apiCallCount(
|
|
252
|
+
event?.callCount ?? usage?.callCount,
|
|
253
|
+
compacted,
|
|
254
|
+
);
|
|
255
|
+
const estimated = Boolean(
|
|
256
|
+
compacted ||
|
|
257
|
+
callCount !== 1
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
if (!rate) return apiUnratedResult(usage, "unknown-model", estimated);
|
|
261
|
+
if (!partition) {
|
|
262
|
+
return apiUnratedResult(usage, "incomplete-token-breakdown", estimated);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const tier = apiServiceTier(serviceTier);
|
|
266
|
+
if (tier === "ultrafast") {
|
|
267
|
+
return apiUnratedResult(usage, "ultrafast-unrated", estimated);
|
|
268
|
+
}
|
|
269
|
+
if (tier === "unsupported") {
|
|
270
|
+
return apiUnratedResult(usage, "unsupported-api-service-tier", estimated);
|
|
271
|
+
}
|
|
272
|
+
if (tier === "fast" && normalizedModel !== "gpt-5.6-sol") {
|
|
273
|
+
return apiUnratedResult(usage, "unsupported-api-fast-tier", estimated);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const inputTokens = partition.uncachedInputTokens +
|
|
277
|
+
partition.cachedInputTokens + partition.cacheWriteInputTokens;
|
|
278
|
+
const originInputTokens = nonNegativeFinite(
|
|
279
|
+
usage?.rangeAllocationOrigin?.inputTokens,
|
|
280
|
+
);
|
|
281
|
+
const proportionalLongContext = normalizedModel === "gpt-5.6-sol" &&
|
|
282
|
+
usage?.rangeAllocationEstimated === true &&
|
|
283
|
+
originInputTokens !== null &&
|
|
284
|
+
originInputTokens > API_USD_LONG_CONTEXT_THRESHOLD_TOKENS;
|
|
285
|
+
const longContext = normalizedModel === "gpt-5.6-sol" &&
|
|
286
|
+
(inputTokens > API_USD_LONG_CONTEXT_THRESHOLD_TOKENS ||
|
|
287
|
+
proportionalLongContext);
|
|
288
|
+
if (longContext && (callCount !== 1 || proportionalLongContext)) {
|
|
289
|
+
return apiUnratedResult(
|
|
290
|
+
usage,
|
|
291
|
+
"compacted-long-context-ambiguous",
|
|
292
|
+
true,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const fastMultiplier = tier === "fast" ? 2 : 1;
|
|
297
|
+
const inputMultiplier = longContext ? 2 : 1;
|
|
298
|
+
const outputMultiplier = longContext ? 1.5 : 1;
|
|
299
|
+
const reasons = [];
|
|
300
|
+
let ratedTokens = partition.uncachedInputTokens +
|
|
301
|
+
partition.cachedInputTokens + partition.outputTokens;
|
|
302
|
+
let unratedTokens = 0;
|
|
303
|
+
let amount = (
|
|
304
|
+
partition.uncachedInputTokens * rate.input * inputMultiplier +
|
|
305
|
+
partition.cachedInputTokens * rate.cached * inputMultiplier +
|
|
306
|
+
partition.outputTokens * rate.output * outputMultiplier
|
|
307
|
+
) * fastMultiplier / 1_000_000;
|
|
308
|
+
|
|
309
|
+
if (partition.cacheWriteInputTokens > 0) {
|
|
310
|
+
if (rate.cacheWrite) {
|
|
311
|
+
ratedTokens += partition.cacheWriteInputTokens;
|
|
312
|
+
amount += partition.cacheWriteInputTokens * rate.input * rate.cacheWrite *
|
|
313
|
+
inputMultiplier * fastMultiplier / 1_000_000;
|
|
314
|
+
} else {
|
|
315
|
+
unratedTokens += partition.cacheWriteInputTokens;
|
|
316
|
+
reasons.push("unsupported-cache-write-price");
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
amount: ratedTokens > 0 ? amount : null,
|
|
322
|
+
currency: "USD",
|
|
323
|
+
ratedTokens,
|
|
324
|
+
unratedTokens,
|
|
325
|
+
complete: unratedTokens === 0,
|
|
326
|
+
estimated,
|
|
327
|
+
reasons,
|
|
328
|
+
partition,
|
|
329
|
+
};
|
|
330
|
+
}
|