@trazum/core 1.39.0 → 1.41.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 +28 -0
- package/dist/connector.d.ts +227 -0
- package/dist/connector.d.ts.map +1 -0
- package/dist/connector.js +431 -0
- package/dist/connector.js.map +1 -0
- package/dist/history.d.ts +110 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +191 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/connector.ts +616 -0
- package/src/history.ts +284 -0
- package/src/index.ts +20 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Your bill, read from the provider, without anybody exporting anything.
|
|
3
|
+
*
|
|
4
|
+
* Every command in this product reads a file somebody produced by hand, and
|
|
5
|
+
* the export step is where adoption dies: the person who would benefit most
|
|
6
|
+
* from a cost report is the person least likely to have a `usage.jsonl` lying
|
|
7
|
+
* around. Every provider that bills by the token also serves that data over an
|
|
8
|
+
* API, and this module turns those payloads into figures the rest of Trazum
|
|
9
|
+
* already knows how to reason about.
|
|
10
|
+
*
|
|
11
|
+
* **Pure, and in the core, so it is testable without a network.** The fetch,
|
|
12
|
+
* the credentials and the pagination live in the CLI — the same split
|
|
13
|
+
* `openrouterOverlay` has had since 1.13. Everything here is a transformation
|
|
14
|
+
* of a document the caller already holds.
|
|
15
|
+
*
|
|
16
|
+
* **The honest part is what the providers cannot tell you.** Usage APIs serve
|
|
17
|
+
* *aggregates*: tokens per bucket per model, and — depending on the provider —
|
|
18
|
+
* a request count or nothing at all. They do not serve per-call rows. That
|
|
19
|
+
* makes a whole class of Trazum's findings impossible on this source: the
|
|
20
|
+
* shape of the calls, the truncation retries, the conversations, the largest
|
|
21
|
+
* call's context pressure. Those findings need per-call data and no amount of
|
|
22
|
+
* arithmetic recovers them from a sum.
|
|
23
|
+
*
|
|
24
|
+
* So a connected report is a **restricted** report, and it is restricted out
|
|
25
|
+
* loud. It carries its own shape rather than a `UsageProfileReport` with holes
|
|
26
|
+
* in it, precisely so a per-call finding can never read a zero this module
|
|
27
|
+
* wrote and report "nothing found" about something nobody measured. Not
|
|
28
|
+
* recorded is not not-happened, at the level of the type system.
|
|
29
|
+
*/
|
|
30
|
+
import { effectivePricing, multipliersFor } from './pricing.js';
|
|
31
|
+
/** Every finding that needs a row per call, named once. */
|
|
32
|
+
const PER_CALL_FINDINGS = [
|
|
33
|
+
{
|
|
34
|
+
finding: 'inputShapes',
|
|
35
|
+
because: 'the provider serves sums over a window, and the spread of call sizes is not in a sum',
|
|
36
|
+
unlockedBy: 'a per-call usage log, or the gateway',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
finding: 'truncationRetries',
|
|
40
|
+
because: 'pairing a truncated answer with its retry needs both calls, their order and their stop reasons',
|
|
41
|
+
unlockedBy: 'a per-call usage log recording stop_reason and session',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
finding: 'repeatedTurns',
|
|
45
|
+
because: 'the same request sent twice is invisible once both are added together',
|
|
46
|
+
unlockedBy: 'a per-call usage log recording session',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
finding: 'sessionCosts',
|
|
50
|
+
because: 'conversations are not a dimension any usage API groups by',
|
|
51
|
+
unlockedBy: 'a per-call usage log recording session',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
finding: 'contextPressure',
|
|
55
|
+
because: 'it reads the largest single call, and a total has lost the maximum',
|
|
56
|
+
unlockedBy: 'a per-call usage log, or the gateway',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
finding: 'duplicateLines',
|
|
60
|
+
because: 'a doubled bill is caught by finding identical rows, and there are no rows here',
|
|
61
|
+
unlockedBy: 'a per-call usage log',
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
/**
|
|
65
|
+
* The two providers this release connects to, and the asymmetry between them
|
|
66
|
+
* that a report must not paper over.
|
|
67
|
+
*
|
|
68
|
+
* OpenAI's usage endpoint serves a request count per bucket; Anthropic's
|
|
69
|
+
* serves token sums without one. So a connected OpenAI report can say "$412
|
|
70
|
+
* over 9,004 calls" and a connected Anthropic report can only say "$412", and
|
|
71
|
+
* every per-call average is available on one and absent on the other. Printing
|
|
72
|
+
* a call count of zero, or dividing by a denominator that does not exist,
|
|
73
|
+
* would be this module inventing the number it is here to stop inventing.
|
|
74
|
+
*/
|
|
75
|
+
export const CONNECTORS = [
|
|
76
|
+
{
|
|
77
|
+
id: 'anthropic',
|
|
78
|
+
displayName: 'Anthropic',
|
|
79
|
+
granularity: 'bucketed',
|
|
80
|
+
credentialEnv: ['TRAZUM_ANTHROPIC_ADMIN_KEY', 'ANTHROPIC_ADMIN_KEY'],
|
|
81
|
+
keyKind: 'an Admin API key (read access to the usage report)',
|
|
82
|
+
servesCallCounts: false,
|
|
83
|
+
unavailable: [
|
|
84
|
+
...PER_CALL_FINDINGS,
|
|
85
|
+
{
|
|
86
|
+
finding: 'calls',
|
|
87
|
+
because: 'the usage report serves token sums per bucket and no request count',
|
|
88
|
+
unlockedBy: 'a per-call usage log, or the gateway',
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
docs: 'https://docs.anthropic.com/en/api/admin-api/usage-cost/get-messages-usage-report',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: 'openai',
|
|
95
|
+
displayName: 'OpenAI',
|
|
96
|
+
granularity: 'bucketed',
|
|
97
|
+
credentialEnv: ['TRAZUM_OPENAI_ADMIN_KEY', 'OPENAI_ADMIN_KEY'],
|
|
98
|
+
keyKind: 'an Admin key with the api.usage.read scope',
|
|
99
|
+
servesCallCounts: true,
|
|
100
|
+
unavailable: PER_CALL_FINDINGS,
|
|
101
|
+
docs: 'https://platform.openai.com/docs/api-reference/usage',
|
|
102
|
+
},
|
|
103
|
+
];
|
|
104
|
+
export function connectorFor(id) {
|
|
105
|
+
return CONNECTORS.find((c) => c.id === id) ?? null;
|
|
106
|
+
}
|
|
107
|
+
// --------------------------------------------------------------------------
|
|
108
|
+
// Normalising provider payloads
|
|
109
|
+
// --------------------------------------------------------------------------
|
|
110
|
+
const num = (value) => typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
111
|
+
const ms = (value) => {
|
|
112
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
113
|
+
return value * 1000;
|
|
114
|
+
if (typeof value !== 'string')
|
|
115
|
+
return null;
|
|
116
|
+
const parsed = Date.parse(value);
|
|
117
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
118
|
+
};
|
|
119
|
+
/** Merges buckets that share a window, model and grouping. */
|
|
120
|
+
function collect(buckets) {
|
|
121
|
+
const merged = new Map();
|
|
122
|
+
for (const bucket of buckets) {
|
|
123
|
+
const key = `${bucket.fromMs}\n${bucket.toMs}\n${bucket.model}\n${JSON.stringify(bucket.group)}`;
|
|
124
|
+
const seen = merged.get(key);
|
|
125
|
+
if (seen === undefined) {
|
|
126
|
+
merged.set(key, { ...bucket });
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
seen.inputTokens += bucket.inputTokens;
|
|
130
|
+
seen.cacheReadTokens += bucket.cacheReadTokens;
|
|
131
|
+
seen.cacheWrite5mTokens += bucket.cacheWrite5mTokens;
|
|
132
|
+
seen.cacheWrite1hTokens += bucket.cacheWrite1hTokens;
|
|
133
|
+
seen.outputTokens += bucket.outputTokens;
|
|
134
|
+
seen.writeTtlKnown = seen.writeTtlKnown && bucket.writeTtlKnown;
|
|
135
|
+
// A count merged with an absent count is still absent: adding a number to
|
|
136
|
+
// "unknown" produces a number that describes only part of the traffic.
|
|
137
|
+
seen.calls = seen.calls === null || bucket.calls === null ? null : seen.calls + bucket.calls;
|
|
138
|
+
}
|
|
139
|
+
return [...merged.values()].sort((a, b) => a.fromMs - b.fromMs || a.model.localeCompare(b.model));
|
|
140
|
+
}
|
|
141
|
+
function windowOf(buckets) {
|
|
142
|
+
if (buckets.length === 0)
|
|
143
|
+
return null;
|
|
144
|
+
return {
|
|
145
|
+
fromMs: Math.min(...buckets.map((b) => b.fromMs)),
|
|
146
|
+
toMs: Math.max(...buckets.map((b) => b.toMs)),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Anthropic's messages usage report.
|
|
151
|
+
*
|
|
152
|
+
* Shape: `{ data: [ { starting_at, ending_at, results: [ {...tokens, model} ] } ] }`.
|
|
153
|
+
* The fields read are the documented ones; anything unreadable is reported as
|
|
154
|
+
* a gap rather than defaulted to zero, because a zero here is a bill that is
|
|
155
|
+
* quietly smaller than the real one.
|
|
156
|
+
*/
|
|
157
|
+
export function normalizeAnthropicUsage(payload) {
|
|
158
|
+
const descriptor = connectorFor('anthropic');
|
|
159
|
+
const gaps = [];
|
|
160
|
+
const buckets = [];
|
|
161
|
+
const data = payload?.data;
|
|
162
|
+
if (!Array.isArray(data)) {
|
|
163
|
+
throw new Error('This payload has no "data" array — is it the response from the Anthropic usage report endpoint?');
|
|
164
|
+
}
|
|
165
|
+
for (const [index, entry] of data.entries()) {
|
|
166
|
+
const row = entry;
|
|
167
|
+
const fromMs = ms(row.starting_at);
|
|
168
|
+
const toMs = ms(row.ending_at) ?? (fromMs === null ? null : fromMs);
|
|
169
|
+
if (fromMs === null || toMs === null) {
|
|
170
|
+
gaps.push({
|
|
171
|
+
kind: 'unreadable-entry',
|
|
172
|
+
detail: `bucket ${index} has no readable time window, so its tokens are in no period and were left out`,
|
|
173
|
+
});
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const results = Array.isArray(row.results) ? row.results : [];
|
|
177
|
+
if (results.length === 0 && row.results !== undefined && !Array.isArray(row.results)) {
|
|
178
|
+
gaps.push({ kind: 'unreadable-entry', detail: `bucket ${index} has an unreadable "results" field` });
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
for (const result of results) {
|
|
182
|
+
const r = result;
|
|
183
|
+
const model = typeof r.model === 'string' ? r.model : null;
|
|
184
|
+
if (model === null) {
|
|
185
|
+
gaps.push({
|
|
186
|
+
kind: 'unreadable-entry',
|
|
187
|
+
detail: `a result in bucket ${index} names no model, so its tokens could not be priced and were left out`,
|
|
188
|
+
});
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const creation = (r.cache_creation ?? {});
|
|
192
|
+
const write5m = num(creation.ephemeral_5m_input_tokens) ?? 0;
|
|
193
|
+
const write1h = num(creation.ephemeral_1h_input_tokens) ?? 0;
|
|
194
|
+
const flatWrite = num(r.cache_creation_input_tokens) ?? 0;
|
|
195
|
+
const ttlKnown = !(flatWrite > 0 && write5m === 0 && write1h === 0);
|
|
196
|
+
buckets.push({
|
|
197
|
+
fromMs,
|
|
198
|
+
toMs,
|
|
199
|
+
model,
|
|
200
|
+
// Documented and deliberate: the usage report has no request count.
|
|
201
|
+
calls: null,
|
|
202
|
+
inputTokens: num(r.uncached_input_tokens) ?? num(r.input_tokens) ?? 0,
|
|
203
|
+
cacheReadTokens: num(r.cache_read_input_tokens) ?? 0,
|
|
204
|
+
cacheWrite5mTokens: ttlKnown ? write5m : flatWrite,
|
|
205
|
+
cacheWrite1hTokens: ttlKnown ? write1h : 0,
|
|
206
|
+
writeTtlKnown: ttlKnown,
|
|
207
|
+
outputTokens: num(r.output_tokens) ?? 0,
|
|
208
|
+
group: groupOf(r, ['workspace_id', 'api_key_id', 'service_tier', 'context_window']),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const merged = collect(buckets);
|
|
213
|
+
return {
|
|
214
|
+
provider: 'anthropic',
|
|
215
|
+
granularity: 'bucketed',
|
|
216
|
+
buckets: merged,
|
|
217
|
+
window: windowOf(merged),
|
|
218
|
+
gaps,
|
|
219
|
+
unavailable: descriptor.unavailable,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* OpenAI's completions usage endpoint.
|
|
224
|
+
*
|
|
225
|
+
* Shape: `{ data: [ { start_time, end_time, results: [ { input_tokens,
|
|
226
|
+
* output_tokens, input_cached_tokens, num_model_requests, model } ] } ] }`.
|
|
227
|
+
*
|
|
228
|
+
* This one serves a request count, so every per-call average is available on
|
|
229
|
+
* it — and the report says so, rather than making both providers look alike.
|
|
230
|
+
*/
|
|
231
|
+
export function normalizeOpenAIUsage(payload) {
|
|
232
|
+
const descriptor = connectorFor('openai');
|
|
233
|
+
const gaps = [];
|
|
234
|
+
const buckets = [];
|
|
235
|
+
const data = payload?.data;
|
|
236
|
+
if (!Array.isArray(data)) {
|
|
237
|
+
throw new Error('This payload has no "data" array — is it the response from the OpenAI usage endpoint?');
|
|
238
|
+
}
|
|
239
|
+
for (const [index, entry] of data.entries()) {
|
|
240
|
+
const row = entry;
|
|
241
|
+
const fromMs = ms(row.start_time);
|
|
242
|
+
const toMs = ms(row.end_time) ?? (fromMs === null ? null : fromMs);
|
|
243
|
+
if (fromMs === null || toMs === null) {
|
|
244
|
+
gaps.push({
|
|
245
|
+
kind: 'unreadable-entry',
|
|
246
|
+
detail: `bucket ${index} has no readable time window, so its tokens are in no period and were left out`,
|
|
247
|
+
});
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const results = Array.isArray(row.results) ? row.results : [];
|
|
251
|
+
for (const result of results) {
|
|
252
|
+
const r = result;
|
|
253
|
+
const model = typeof r.model === 'string' ? r.model : null;
|
|
254
|
+
if (model === null) {
|
|
255
|
+
gaps.push({
|
|
256
|
+
kind: 'unreadable-entry',
|
|
257
|
+
detail: `a result in bucket ${index} names no model, so its tokens could not be priced and were left out`,
|
|
258
|
+
});
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const cached = num(r.input_cached_tokens) ?? 0;
|
|
262
|
+
const input = num(r.input_tokens) ?? 0;
|
|
263
|
+
buckets.push({
|
|
264
|
+
fromMs,
|
|
265
|
+
toMs,
|
|
266
|
+
model,
|
|
267
|
+
calls: num(r.num_model_requests),
|
|
268
|
+
// OpenAI reports cached tokens *inside* the input total, so the
|
|
269
|
+
// uncached half is the subtraction. Reporting both at face value
|
|
270
|
+
// would bill the cached tokens twice, at the dearer rate.
|
|
271
|
+
inputTokens: Math.max(0, input - cached),
|
|
272
|
+
cacheReadTokens: cached,
|
|
273
|
+
cacheWrite5mTokens: 0,
|
|
274
|
+
cacheWrite1hTokens: 0,
|
|
275
|
+
// Nothing was assumed: this API reports no cache writes at all, and an
|
|
276
|
+
// absent field is not an assumed TTL.
|
|
277
|
+
writeTtlKnown: true,
|
|
278
|
+
outputTokens: num(r.output_tokens) ?? 0,
|
|
279
|
+
group: groupOf(r, ['project_id', 'api_key_id', 'batch']),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const merged = collect(buckets);
|
|
284
|
+
return {
|
|
285
|
+
provider: 'openai',
|
|
286
|
+
granularity: 'bucketed',
|
|
287
|
+
buckets: merged,
|
|
288
|
+
window: windowOf(merged),
|
|
289
|
+
gaps,
|
|
290
|
+
unavailable: descriptor.unavailable,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function groupOf(row, keys) {
|
|
294
|
+
const group = {};
|
|
295
|
+
for (const key of keys) {
|
|
296
|
+
const value = row[key];
|
|
297
|
+
if (typeof value === 'string' && value !== '')
|
|
298
|
+
group[key] = value;
|
|
299
|
+
else if (typeof value === 'number' && Number.isFinite(value))
|
|
300
|
+
group[key] = String(value);
|
|
301
|
+
else if (typeof value === 'boolean')
|
|
302
|
+
group[key] = String(value);
|
|
303
|
+
}
|
|
304
|
+
return group;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Prices the buckets a connector pulled.
|
|
308
|
+
*
|
|
309
|
+
* Every figure here is the provider's own billed token count at the
|
|
310
|
+
* catalogue's rates — the same arithmetic `profile` does, over sums instead of
|
|
311
|
+
* rows. What it deliberately does not do is synthesise the per-call findings:
|
|
312
|
+
* they are listed as unavailable and left absent, so nothing downstream can
|
|
313
|
+
* read a zero this function wrote.
|
|
314
|
+
*/
|
|
315
|
+
export function bucketedProfile(pull, options) {
|
|
316
|
+
const { catalogue, on = new Date() } = options;
|
|
317
|
+
const slices = new Map();
|
|
318
|
+
const days = new Map();
|
|
319
|
+
const unpriced = new Map();
|
|
320
|
+
for (const bucket of pull.buckets) {
|
|
321
|
+
const model = catalogue.byId.get(bucket.model);
|
|
322
|
+
if (model === undefined) {
|
|
323
|
+
const seen = unpriced.get(bucket.model) ?? { inputTokens: 0, outputTokens: 0 };
|
|
324
|
+
seen.inputTokens += bucket.inputTokens + bucket.cacheReadTokens;
|
|
325
|
+
seen.outputTokens += bucket.outputTokens;
|
|
326
|
+
unpriced.set(bucket.model, seen);
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const { inputPerMTok, outputPerMTok } = effectivePricing(model, on);
|
|
330
|
+
const rates = multipliersFor(model);
|
|
331
|
+
const per = (count, rate) => (count / 1_000_000) * rate;
|
|
332
|
+
const inputUsd = per(bucket.inputTokens, inputPerMTok);
|
|
333
|
+
const cacheReadUsd = per(bucket.cacheReadTokens, inputPerMTok * rates.cacheRead);
|
|
334
|
+
const cacheWriteUsd = per(bucket.cacheWrite5mTokens, inputPerMTok * rates.cacheWrite5m) +
|
|
335
|
+
per(bucket.cacheWrite1hTokens, inputPerMTok * rates.cacheWrite1h);
|
|
336
|
+
const outputUsd = per(bucket.outputTokens, outputPerMTok);
|
|
337
|
+
const writeTokens = bucket.cacheWrite5mTokens + bucket.cacheWrite1hTokens;
|
|
338
|
+
const atInputRate = per(bucket.cacheReadTokens + writeTokens, inputPerMTok);
|
|
339
|
+
const ifAssumed1h = bucket.writeTtlKnown
|
|
340
|
+
? cacheWriteUsd
|
|
341
|
+
: per(writeTokens, inputPerMTok * rates.cacheWrite1h);
|
|
342
|
+
const slice = slices.get(bucket.model) ?? {
|
|
343
|
+
model: bucket.model,
|
|
344
|
+
calls: bucket.calls === null ? null : 0,
|
|
345
|
+
inputTokens: 0,
|
|
346
|
+
cacheReadTokens: 0,
|
|
347
|
+
cacheWriteTokens: 0,
|
|
348
|
+
outputTokens: 0,
|
|
349
|
+
inputUsd: 0,
|
|
350
|
+
cacheReadUsd: 0,
|
|
351
|
+
cacheWriteUsd: 0,
|
|
352
|
+
outputUsd: 0,
|
|
353
|
+
totalUsd: 0,
|
|
354
|
+
cachedTokensAtInputRateUsd: 0,
|
|
355
|
+
cacheWriteUsdIfAssumed1h: 0,
|
|
356
|
+
writeTtlKnown: true,
|
|
357
|
+
};
|
|
358
|
+
slice.calls = slice.calls === null || bucket.calls === null ? null : slice.calls + bucket.calls;
|
|
359
|
+
slice.inputTokens += bucket.inputTokens;
|
|
360
|
+
slice.cacheReadTokens += bucket.cacheReadTokens;
|
|
361
|
+
slice.cacheWriteTokens += writeTokens;
|
|
362
|
+
slice.outputTokens += bucket.outputTokens;
|
|
363
|
+
slice.inputUsd += inputUsd;
|
|
364
|
+
slice.cacheReadUsd += cacheReadUsd;
|
|
365
|
+
slice.cacheWriteUsd += cacheWriteUsd;
|
|
366
|
+
slice.outputUsd += outputUsd;
|
|
367
|
+
slice.totalUsd += inputUsd + cacheReadUsd + cacheWriteUsd + outputUsd;
|
|
368
|
+
slice.cachedTokensAtInputRateUsd += atInputRate;
|
|
369
|
+
slice.cacheWriteUsdIfAssumed1h += ifAssumed1h;
|
|
370
|
+
slice.writeTtlKnown = slice.writeTtlKnown && bucket.writeTtlKnown;
|
|
371
|
+
slices.set(bucket.model, slice);
|
|
372
|
+
const day = new Date(bucket.fromMs).toISOString().slice(0, 10);
|
|
373
|
+
const entry = days.get(day) ?? { usd: 0, calls: bucket.calls === null ? null : 0 };
|
|
374
|
+
entry.usd += inputUsd + cacheReadUsd + cacheWriteUsd + outputUsd;
|
|
375
|
+
entry.calls = entry.calls === null || bucket.calls === null ? null : entry.calls + bucket.calls;
|
|
376
|
+
days.set(day, entry);
|
|
377
|
+
}
|
|
378
|
+
const byModel = [...slices.values()].sort((a, b) => b.totalUsd - a.totalUsd);
|
|
379
|
+
const anyCallsUnknown = byModel.some((s) => s.calls === null) || byModel.length === 0;
|
|
380
|
+
return {
|
|
381
|
+
schemaVersion: 1,
|
|
382
|
+
provider: pull.provider,
|
|
383
|
+
granularity: pull.granularity,
|
|
384
|
+
span: pull.window,
|
|
385
|
+
total: {
|
|
386
|
+
totalUsd: byModel.reduce((sum, s) => sum + s.totalUsd, 0),
|
|
387
|
+
calls: anyCallsUnknown ? null : byModel.reduce((sum, s) => sum + (s.calls ?? 0), 0),
|
|
388
|
+
inputTokens: byModel.reduce((sum, s) => sum + s.inputTokens, 0),
|
|
389
|
+
cacheReadTokens: byModel.reduce((sum, s) => sum + s.cacheReadTokens, 0),
|
|
390
|
+
cacheWriteTokens: byModel.reduce((sum, s) => sum + s.cacheWriteTokens, 0),
|
|
391
|
+
outputTokens: byModel.reduce((sum, s) => sum + s.outputTokens, 0),
|
|
392
|
+
},
|
|
393
|
+
byModel,
|
|
394
|
+
byDay: [...days.entries()]
|
|
395
|
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
396
|
+
.map(([day, entry]) => ({ day, usd: entry.usd, calls: entry.calls })),
|
|
397
|
+
unpricedModels: [...unpriced.entries()]
|
|
398
|
+
.map(([model, tokens]) => ({ model, ...tokens }))
|
|
399
|
+
.sort((a, b) => b.inputTokens + b.outputTokens - (a.inputTokens + a.outputTokens)),
|
|
400
|
+
gaps: pull.gaps,
|
|
401
|
+
unavailable: pull.unavailable,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* The cache verdict over a connected report.
|
|
406
|
+
*
|
|
407
|
+
* Same counterfactual `cacheEconomics` runs on a per-call report: what the
|
|
408
|
+
* cache-touched tokens cost, against what they would have cost as ordinary
|
|
409
|
+
* input. The worst case is carried separately for the same reason it is
|
|
410
|
+
* there — when the source did not state the write TTL, the cheaper rate was
|
|
411
|
+
* assumed for the headline and the verdict can move under the other one.
|
|
412
|
+
*/
|
|
413
|
+
export function bucketedCacheEconomics(report) {
|
|
414
|
+
const spent = report.byModel.reduce((sum, s) => sum + s.cacheReadUsd + s.cacheWriteUsd, 0);
|
|
415
|
+
const without = report.byModel.reduce((sum, s) => sum + s.cachedTokensAtInputRateUsd, 0);
|
|
416
|
+
const worst = report.byModel.reduce((sum, s) => sum + s.cacheReadUsd + s.cacheWriteUsdIfAssumed1h, 0);
|
|
417
|
+
const touched = report.byModel.reduce((sum, s) => sum + s.cacheReadTokens + s.cacheWriteTokens, 0);
|
|
418
|
+
const verdictOf = (paid) => {
|
|
419
|
+
if (touched === 0)
|
|
420
|
+
return 'no-cache';
|
|
421
|
+
return paid <= without ? 'paid-off' : 'lost-money';
|
|
422
|
+
};
|
|
423
|
+
return {
|
|
424
|
+
spentUsd: spent,
|
|
425
|
+
withoutCachingUsd: without,
|
|
426
|
+
deltaUsd: spent - without,
|
|
427
|
+
verdict: verdictOf(spent),
|
|
428
|
+
worstCaseVerdict: verdictOf(worst),
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
//# sourceMappingURL=connector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connector.js","sourceRoot":"","sources":["../src/connector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAyBhE,2DAA2D;AAC3D,MAAM,iBAAiB,GAAkC;IACvD;QACE,OAAO,EAAE,aAAa;QACtB,OAAO,EAAE,sFAAsF;QAC/F,UAAU,EAAE,sCAAsC;KACnD;IACD;QACE,OAAO,EAAE,mBAAmB;QAC5B,OAAO,EAAE,gGAAgG;QACzG,UAAU,EAAE,wDAAwD;KACrE;IACD;QACE,OAAO,EAAE,eAAe;QACxB,OAAO,EAAE,uEAAuE;QAChF,UAAU,EAAE,wCAAwC;KACrD;IACD;QACE,OAAO,EAAE,cAAc;QACvB,OAAO,EAAE,2DAA2D;QACpE,UAAU,EAAE,wCAAwC;KACrD;IACD;QACE,OAAO,EAAE,iBAAiB;QAC1B,OAAO,EAAE,oEAAoE;QAC7E,UAAU,EAAE,sCAAsC;KACnD;IACD;QACE,OAAO,EAAE,gBAAgB;QACzB,OAAO,EAAE,gFAAgF;QACzF,UAAU,EAAE,sBAAsB;KACnC;CACF,CAAC;AAwBF;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,UAAU,GAAmC;IACxD;QACE,EAAE,EAAE,WAAW;QACf,WAAW,EAAE,WAAW;QACxB,WAAW,EAAE,UAAU;QACvB,aAAa,EAAE,CAAC,4BAA4B,EAAE,qBAAqB,CAAC;QACpE,OAAO,EAAE,oDAAoD;QAC7D,gBAAgB,EAAE,KAAK;QACvB,WAAW,EAAE;YACX,GAAG,iBAAiB;YACpB;gBACE,OAAO,EAAE,OAAO;gBAChB,OAAO,EAAE,oEAAoE;gBAC7E,UAAU,EAAE,sCAAsC;aACnD;SACF;QACD,IAAI,EAAE,kFAAkF;KACzF;IACD;QACE,EAAE,EAAE,QAAQ;QACZ,WAAW,EAAE,QAAQ;QACrB,WAAW,EAAE,UAAU;QACvB,aAAa,EAAE,CAAC,yBAAyB,EAAE,kBAAkB,CAAC;QAC9D,OAAO,EAAE,4CAA4C;QACrD,gBAAgB,EAAE,IAAI;QACtB,WAAW,EAAE,iBAAiB;QAC9B,IAAI,EAAE,sDAAsD;KAC7D;CACF,CAAC;AAEF,MAAM,UAAU,YAAY,CAAC,EAAU;IACrC,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,IAAI,IAAI,CAAC;AACrD,CAAC;AA0DD,6EAA6E;AAC7E,gCAAgC;AAChC,6EAA6E;AAE7E,MAAM,GAAG,GAAG,CAAC,KAAc,EAAiB,EAAE,CAC5C,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AAErE,MAAM,EAAE,GAAG,CAAC,KAAc,EAAiB,EAAE;IAC3C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,GAAG,IAAI,CAAC;IAC7E,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC3C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACjC,OAAO,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC;AAC9C,CAAC,CAAC;AAEF,8DAA8D;AAC9D,SAAS,OAAO,CAAC,OAAsB;IACrC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAuB,CAAC;IAC9C,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,GAAG,GAAG,GAAG,MAAM,CAAC,MAAM,KAAK,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,KAAK,KAAK,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QACjG,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC7B,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACvB,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,GAAG,MAAM,EAAE,CAAC,CAAC;YAC/B,SAAS;QACX,CAAC;QACD,IAAI,CAAC,WAAW,IAAI,MAAM,CAAC,WAAW,CAAC;QACvC,IAAI,CAAC,eAAe,IAAI,MAAM,CAAC,eAAe,CAAC;QAC/C,IAAI,CAAC,kBAAkB,IAAI,MAAM,CAAC,kBAAkB,CAAC;QACrD,IAAI,CAAC,kBAAkB,IAAI,MAAM,CAAC,kBAAkB,CAAC;QACrD,IAAI,CAAC,YAAY,IAAI,MAAM,CAAC,YAAY,CAAC;QACzC,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,aAAa,IAAI,MAAM,CAAC,aAAa,CAAC;QAChE,0EAA0E;QAC1E,uEAAuE;QACvE,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,KAAK,IAAI,IAAI,MAAM,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;IAC/F,CAAC;IACD,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;AACpG,CAAC;AAED,SAAS,QAAQ,CAAC,OAAsB;IACtC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACtC,OAAO;QACL,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;QACjD,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;KAC9C,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,uBAAuB,CAAC,OAAgB;IACtD,MAAM,UAAU,GAAG,YAAY,CAAC,WAAW,CAAE,CAAC;IAC9C,MAAM,IAAI,GAAc,EAAE,CAAC;IAC3B,MAAM,OAAO,GAAkB,EAAE,CAAC;IAElC,MAAM,IAAI,GAAI,OAA8B,EAAE,IAAI,CAAC;IACnD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CACb,iGAAiG,CAClG,CAAC;IACJ,CAAC;IAED,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC;QAC5C,MAAM,GAAG,GAAG,KAA0E,CAAC;QACvF,MAAM,MAAM,GAAG,EAAE,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QACnC,MAAM,IAAI,GAAG,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;QACpE,IAAI,MAAM,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YACrC,IAAI,CAAC,IAAI,CAAC;gBACR,IAAI,EAAE,kBAAkB;gBACxB,MAAM,EAAE,UAAU,KAAK,gFAAgF;aACxG,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QACD,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9D,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,GAAG,CAAC,OAAO,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YACrF,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,kBAAkB,EAAE,MAAM,EAAE,UAAU,KAAK,oCAAoC,EAAE,CAAC,CAAC;YACrG,SAAS;QACX,CAAC;QACD,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC7B,MAAM,CAAC,GAAG,MAAiC,CAAC;YAC5C,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;YAC3D,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;gBACnB,IAAI,CAAC,IAAI,CAAC;oBACR,IAAI,EAAE,kBAAkB;oBACxB,MAAM,EAAE,sBAAsB,KAAK,sEAAsE;iBAC1G,CAAC,CAAC;gBACH,SAAS;YACX,CAAC;YACD,MAAM,QAAQ,GAAG,CAAC,CAAC,CAAC,cAAc,IAAI,EAAE,CAA4B,CAAC;YACrE,MAAM,OAAO,GAAG,GAAG,CAAC,QAAQ,CAAC,yBAAyB,CAAC,IAAI,CAAC,CAAC;YAC7D,MAAM,OAAO,GAAG,GAAG,CAAC,QAAQ,CAAC,yBAAyB,CAAC,IAAI,CAAC,CAAC;YAC7D,MAAM,SAAS,GAAG,GAAG,CAAC,CAAC,CAAC,2BAA2B,CAAC,IAAI,CAAC,CAAC;YAC1D,MAAM,QAAQ,GAAG,CAAC,CAAC,SAAS,GAAG,CAAC,IAAI,OAAO,KAAK,CAAC,IAAI,OAAO,KAAK,CAAC,CAAC,CAAC;YACpE,OAAO,CAAC,IAAI,CAAC;gBACX,MAAM;gBACN,IAAI;gBACJ,KAAK;gBACL,oEAAoE;gBACpE,KAAK,EAAE,IAAI;gBACX,WAAW,EAAE,GAAG,CAAC,CAAC,CAAC,qBAAqB,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC;gBACrE,eAAe,EAAE,GAAG,CAAC,CAAC,CAAC,uBAAuB,CAAC,IAAI,CAAC;gBACpD,kBAAkB,EAAE,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS;gBAClD,kBAAkB,EAAE,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;gBAC1C,aAAa,EAAE,QAAQ;gBACvB,YAAY,EAAE,GAAG,CAAC,CAAC,CAAC,aAAa,CAAC,IAAI,CAAC;gBACvC,KAAK,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC,cAAc,EAAE,YAAY,EAAE,cAAc,EAAE,gBAAgB,CAAC,CAAC;aACpF,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAChC,OAAO;QACL,QAAQ,EAAE,WAAW;QACrB,WAAW,EAAE,UAAU;QACvB,OAAO,EAAE,MAAM;QACf,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC;QACxB,IAAI;QACJ,WAAW,EAAE,UAAU,CAAC,WAAW;KACpC,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAgB;IACnD,MAAM,UAAU,GAAG,YAAY,CAAC,QAAQ,CAAE,CAAC;IAC3C,MAAM,IAAI,GAAc,EAAE,CAAC;IAC3B,MAAM,OAAO,GAAkB,EAAE,CAAC;IAElC,MAAM,IAAI,GAAI,OAA8B,EAAE,IAAI,CAAC;IACnD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CACb,uFAAuF,CACxF,CAAC;IACJ,CAAC;IAED,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC;QAC5C,MAAM,GAAG,GAAG,KAAwE,CAAC;QACrF,MAAM,MAAM,GAAG,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAClC,MAAM,IAAI,GAAG,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;QACnE,IAAI,MAAM,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YACrC,IAAI,CAAC,IAAI,CAAC;gBACR,IAAI,EAAE,kBAAkB;gBACxB,MAAM,EAAE,UAAU,KAAK,gFAAgF;aACxG,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QACD,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9D,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC7B,MAAM,CAAC,GAAG,MAAiC,CAAC;YAC5C,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;YAC3D,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;gBACnB,IAAI,CAAC,IAAI,CAAC;oBACR,IAAI,EAAE,kBAAkB;oBACxB,MAAM,EAAE,sBAAsB,KAAK,sEAAsE;iBAC1G,CAAC,CAAC;gBACH,SAAS;YACX,CAAC;YACD,MAAM,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC;YAC/C,MAAM,KAAK,GAAG,GAAG,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;YACvC,OAAO,CAAC,IAAI,CAAC;gBACX,MAAM;gBACN,IAAI;gBACJ,KAAK;gBACL,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,kBAAkB,CAAC;gBAChC,gEAAgE;gBAChE,iEAAiE;gBACjE,0DAA0D;gBAC1D,WAAW,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;gBACxC,eAAe,EAAE,MAAM;gBACvB,kBAAkB,EAAE,CAAC;gBACrB,kBAAkB,EAAE,CAAC;gBACrB,uEAAuE;gBACvE,sCAAsC;gBACtC,aAAa,EAAE,IAAI;gBACnB,YAAY,EAAE,GAAG,CAAC,CAAC,CAAC,aAAa,CAAC,IAAI,CAAC;gBACvC,KAAK,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC,YAAY,EAAE,YAAY,EAAE,OAAO,CAAC,CAAC;aACzD,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAChC,OAAO;QACL,QAAQ,EAAE,QAAQ;QAClB,WAAW,EAAE,UAAU;QACvB,OAAO,EAAE,MAAM;QACf,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC;QACxB,IAAI;QACJ,WAAW,EAAE,UAAU,CAAC,WAAW;KACpC,CAAC;AACJ,CAAC;AAED,SAAS,OAAO,CAAC,GAA4B,EAAE,IAAuB;IACpE,MAAM,KAAK,GAA2B,EAAE,CAAC;IACzC,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;QACvB,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,EAAE;YAAE,KAAK,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;aAC7D,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;YAAE,KAAK,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;aACpF,IAAI,OAAO,KAAK,KAAK,SAAS;YAAE,KAAK,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAClE,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAiDD;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAC7B,IAAmB,EACnB,OAAmD;IAEnD,MAAM,EAAE,SAAS,EAAE,EAAE,GAAG,IAAI,IAAI,EAAE,EAAE,GAAG,OAAO,CAAC;IAE/C,MAAM,MAAM,GAAG,IAAI,GAAG,EAAyB,CAAC;IAChD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAiD,CAAC;IACtE,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAyD,CAAC;IAElF,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QAClC,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC/C,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,WAAW,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC;YAC/E,IAAI,CAAC,WAAW,IAAI,MAAM,CAAC,WAAW,GAAG,MAAM,CAAC,eAAe,CAAC;YAChE,IAAI,CAAC,YAAY,IAAI,MAAM,CAAC,YAAY,CAAC;YACzC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YACjC,SAAS;QACX,CAAC;QAED,MAAM,EAAE,YAAY,EAAE,aAAa,EAAE,GAAG,gBAAgB,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QACpE,MAAM,KAAK,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;QACpC,MAAM,GAAG,GAAG,CAAC,KAAa,EAAE,IAAY,EAAU,EAAE,CAAC,CAAC,KAAK,GAAG,SAAS,CAAC,GAAG,IAAI,CAAC;QAEhF,MAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;QACvD,MAAM,YAAY,GAAG,GAAG,CAAC,MAAM,CAAC,eAAe,EAAE,YAAY,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC;QACjF,MAAM,aAAa,GACjB,GAAG,CAAC,MAAM,CAAC,kBAAkB,EAAE,YAAY,GAAG,KAAK,CAAC,YAAY,CAAC;YACjE,GAAG,CAAC,MAAM,CAAC,kBAAkB,EAAE,YAAY,GAAG,KAAK,CAAC,YAAY,CAAC,CAAC;QACpE,MAAM,SAAS,GAAG,GAAG,CAAC,MAAM,CAAC,YAAY,EAAE,aAAa,CAAC,CAAC;QAC1D,MAAM,WAAW,GAAG,MAAM,CAAC,kBAAkB,GAAG,MAAM,CAAC,kBAAkB,CAAC;QAC1E,MAAM,WAAW,GAAG,GAAG,CAAC,MAAM,CAAC,eAAe,GAAG,WAAW,EAAE,YAAY,CAAC,CAAC;QAC5E,MAAM,WAAW,GAAG,MAAM,CAAC,aAAa;YACtC,CAAC,CAAC,aAAa;YACf,CAAC,CAAC,GAAG,CAAC,WAAW,EAAE,YAAY,GAAG,KAAK,CAAC,YAAY,CAAC,CAAC;QAExD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI;YACxC,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,KAAK,EAAE,MAAM,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACvC,WAAW,EAAE,CAAC;YACd,eAAe,EAAE,CAAC;YAClB,gBAAgB,EAAE,CAAC;YACnB,YAAY,EAAE,CAAC;YACf,QAAQ,EAAE,CAAC;YACX,YAAY,EAAE,CAAC;YACf,aAAa,EAAE,CAAC;YAChB,SAAS,EAAE,CAAC;YACZ,QAAQ,EAAE,CAAC;YACX,0BAA0B,EAAE,CAAC;YAC7B,wBAAwB,EAAE,CAAC;YAC3B,aAAa,EAAE,IAAI;SACpB,CAAC;QACF,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,KAAK,IAAI,IAAI,MAAM,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;QAChG,KAAK,CAAC,WAAW,IAAI,MAAM,CAAC,WAAW,CAAC;QACxC,KAAK,CAAC,eAAe,IAAI,MAAM,CAAC,eAAe,CAAC;QAChD,KAAK,CAAC,gBAAgB,IAAI,WAAW,CAAC;QACtC,KAAK,CAAC,YAAY,IAAI,MAAM,CAAC,YAAY,CAAC;QAC1C,KAAK,CAAC,QAAQ,IAAI,QAAQ,CAAC;QAC3B,KAAK,CAAC,YAAY,IAAI,YAAY,CAAC;QACnC,KAAK,CAAC,aAAa,IAAI,aAAa,CAAC;QACrC,KAAK,CAAC,SAAS,IAAI,SAAS,CAAC;QAC7B,KAAK,CAAC,QAAQ,IAAI,QAAQ,GAAG,YAAY,GAAG,aAAa,GAAG,SAAS,CAAC;QACtE,KAAK,CAAC,0BAA0B,IAAI,WAAW,CAAC;QAChD,KAAK,CAAC,wBAAwB,IAAI,WAAW,CAAC;QAC9C,KAAK,CAAC,aAAa,GAAG,KAAK,CAAC,aAAa,IAAI,MAAM,CAAC,aAAa,CAAC;QAClE,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QAEhC,MAAM,GAAG,GAAG,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC/D,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACnF,KAAK,CAAC,GAAG,IAAI,QAAQ,GAAG,YAAY,GAAG,aAAa,GAAG,SAAS,CAAC;QACjE,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,KAAK,IAAI,IAAI,MAAM,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;QAChG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACvB,CAAC;IAED,MAAM,OAAO,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC;IAC7E,MAAM,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,IAAI,CAAC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC;IAEtF,OAAO;QACL,aAAa,EAAE,CAAC;QAChB,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,IAAI,EAAE,IAAI,CAAC,MAAM;QACjB,KAAK,EAAE;YACL,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;YACzD,KAAK,EAAE,eAAe,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC;YACnF,WAAW,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;YAC/D,eAAe,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,eAAe,EAAE,CAAC,CAAC;YACvE,gBAAgB,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,gBAAgB,EAAE,CAAC,CAAC;YACzE,YAAY,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,YAAY,EAAE,CAAC,CAAC;SAClE;QACD,OAAO;QACP,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;aACvB,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;aACxC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;QACvE,cAAc,EAAE,CAAC,GAAG,QAAQ,CAAC,OAAO,EAAE,CAAC;aACpC,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,MAAM,EAAE,CAAC,CAAC;aAChD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,GAAG,CAAC,CAAC,YAAY,GAAG,CAAC,CAAC,CAAC,WAAW,GAAG,CAAC,CAAC,YAAY,CAAC,CAAC;QACpF,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,WAAW,EAAE,IAAI,CAAC,WAAW;KAC9B,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAAsB;IAO3D,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,YAAY,GAAG,CAAC,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC;IAC3F,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,0BAA0B,EAAE,CAAC,CAAC,CAAC;IACzF,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,YAAY,GAAG,CAAC,CAAC,wBAAwB,EAAE,CAAC,CAAC,CAAC;IACtG,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,eAAe,GAAG,CAAC,CAAC,gBAAgB,EAAE,CAAC,CAAC,CAAC;IAEnG,MAAM,SAAS,GAAG,CAAC,IAAY,EAA0C,EAAE;QACzE,IAAI,OAAO,KAAK,CAAC;YAAE,OAAO,UAAU,CAAC;QACrC,OAAO,IAAI,IAAI,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,YAAY,CAAC;IACrD,CAAC,CAAC;IAEF,OAAO;QACL,QAAQ,EAAE,KAAK;QACf,iBAAiB,EAAE,OAAO;QAC1B,QAAQ,EAAE,KAAK,GAAG,OAAO;QACzB,OAAO,EAAE,SAAS,CAAC,KAAK,CAAC;QACzB,gBAAgB,EAAE,SAAS,CAAC,KAAK,CAAC;KACnC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The long run: many reports over many periods, as one series.
|
|
3
|
+
*
|
|
4
|
+
* Every comparison in Trazum is between two logs, and a product's cost
|
|
5
|
+
* problem is rarely visible in two — it is visible in twenty. This module
|
|
6
|
+
* takes *stored reports* (the `--json` documents a team already keeps) and
|
|
7
|
+
* builds the series no pairwise comparison can see: the workload that grew a
|
|
8
|
+
* little every week, the model share that has been climbing since a date,
|
|
9
|
+
* the cache hit rate decaying slowly enough that no single week's report
|
|
10
|
+
* called it a finding.
|
|
11
|
+
*
|
|
12
|
+
* **Still no forecasts.** Twenty points make a trend visible; they do not
|
|
13
|
+
* make next month knowable. The series is stated, the shape is named as
|
|
14
|
+
* consecutive movement — never a line fitted through the points — and where
|
|
15
|
+
* it goes next remains the reader's to judge, the same refusal
|
|
16
|
+
* `modelMixDrift` has carried since 1.27.
|
|
17
|
+
*
|
|
18
|
+
* **Derived from stored reports, not re-parsed logs**, so a year of `--json`
|
|
19
|
+
* output is enough and the raw logs can be thrown away — which is what the
|
|
20
|
+
* privacy story requires anyway. Browser-safe: documents in, series out.
|
|
21
|
+
*/
|
|
22
|
+
import type { PlanActionKind, PlanDocument } from './plan.js';
|
|
23
|
+
/** The slice of a stored profile document this module actually reads. */
|
|
24
|
+
export interface StoredReport {
|
|
25
|
+
/** Where it came from — a file name, shown so a finding can be traced. */
|
|
26
|
+
name: string;
|
|
27
|
+
span: {
|
|
28
|
+
fromMs: number;
|
|
29
|
+
toMs: number;
|
|
30
|
+
} | null;
|
|
31
|
+
totalUsd: number;
|
|
32
|
+
calls: number;
|
|
33
|
+
/** Label → dollars this period. */
|
|
34
|
+
byLabel: Map<string, number>;
|
|
35
|
+
/** Model → dollars this period. */
|
|
36
|
+
byModel: Map<string, number>;
|
|
37
|
+
/** Share of input tokens served from cache, or null when unknowable. */
|
|
38
|
+
cacheReadShare: number | null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A run of consecutive movement, named — never extrapolated.
|
|
42
|
+
*
|
|
43
|
+
* `periods` counts the *rises* (or falls), so a run of 3 spans 4 reports.
|
|
44
|
+
* The floor is 3: two rises is what `--against` already shows, and one is
|
|
45
|
+
* noise wearing a trend's clothes.
|
|
46
|
+
*/
|
|
47
|
+
export interface HistoryRun {
|
|
48
|
+
kind: 'label-spend-climbing' | 'model-share-climbing' | 'cache-share-decaying';
|
|
49
|
+
subject: string;
|
|
50
|
+
/** Consecutive rises (falls, for decay). */
|
|
51
|
+
periods: number;
|
|
52
|
+
/** The report the run started in, by name — "climbing since <this one>". */
|
|
53
|
+
sinceName: string;
|
|
54
|
+
/** First and last values of the run, so the reader judges the size. */
|
|
55
|
+
from: number;
|
|
56
|
+
to: number;
|
|
57
|
+
}
|
|
58
|
+
/** The same action planned again and again: a decision nobody is executing. */
|
|
59
|
+
export interface RepeatedPlanAction {
|
|
60
|
+
kind: PlanActionKind;
|
|
61
|
+
label: string;
|
|
62
|
+
model: string;
|
|
63
|
+
appearances: number;
|
|
64
|
+
firstPlanned: string | null;
|
|
65
|
+
lastPlanned: string | null;
|
|
66
|
+
}
|
|
67
|
+
export interface HistoryDocument {
|
|
68
|
+
schemaVersion: 1;
|
|
69
|
+
/** Ordered oldest first by span start. */
|
|
70
|
+
periods: {
|
|
71
|
+
name: string;
|
|
72
|
+
fromMs: number;
|
|
73
|
+
toMs: number;
|
|
74
|
+
totalUsd: number;
|
|
75
|
+
calls: number;
|
|
76
|
+
}[];
|
|
77
|
+
/** Per label, dollars per period — null where the label had no traffic. */
|
|
78
|
+
labelSeries: {
|
|
79
|
+
label: string;
|
|
80
|
+
points: (number | null)[];
|
|
81
|
+
}[];
|
|
82
|
+
/** Per model, share of that period's total — null where absent. */
|
|
83
|
+
modelShareSeries: {
|
|
84
|
+
model: string;
|
|
85
|
+
points: (number | null)[];
|
|
86
|
+
}[];
|
|
87
|
+
/** Cache read share per period, null where unknowable. */
|
|
88
|
+
cacheShareSeries: (number | null)[];
|
|
89
|
+
/** The findings only a series can make. Shapes, never forecasts. */
|
|
90
|
+
runs: HistoryRun[];
|
|
91
|
+
/** Plans in the same directory, held against each other. */
|
|
92
|
+
repeatedPlanActions: RepeatedPlanAction[];
|
|
93
|
+
/**
|
|
94
|
+
* Reports that carry no span cannot be placed on a timeline; they are
|
|
95
|
+
* named here and in no series above, never silently absorbed.
|
|
96
|
+
*/
|
|
97
|
+
undatedReports: string[];
|
|
98
|
+
}
|
|
99
|
+
export declare const MIN_RUN = 3;
|
|
100
|
+
export declare function buildHistory(reports: StoredReport[], plans?: (PlanDocument & {
|
|
101
|
+
createdAt?: string;
|
|
102
|
+
name?: string;
|
|
103
|
+
})[]): HistoryDocument;
|
|
104
|
+
/**
|
|
105
|
+
* Reads one stored `profile --json` document into the slice history needs.
|
|
106
|
+
* Returns null when the JSON is not a profile document — the caller names
|
|
107
|
+
* the file rather than absorbing it.
|
|
108
|
+
*/
|
|
109
|
+
export declare function storedReportFrom(name: string, parsed: unknown): StoredReport | null;
|
|
110
|
+
//# sourceMappingURL=history.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"history.d.ts","sourceRoot":"","sources":["../src/history.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE9D,yEAAyE;AACzE,MAAM,WAAW,YAAY;IAC3B,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,mCAAmC;IACnC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,mCAAmC;IACnC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,wEAAwE;IACxE,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,sBAAsB,GAAG,sBAAsB,GAAG,sBAAsB,CAAC;IAC/E,OAAO,EAAE,MAAM,CAAC;IAChB,4CAA4C;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,SAAS,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,+EAA+E;AAC/E,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,cAAc,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,CAAC,CAAC;IACjB,0CAA0C;IAC1C,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC3F,2EAA2E;IAC3E,WAAW,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAA;KAAE,EAAE,CAAC;IAC5D,mEAAmE;IACnE,gBAAgB,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAA;KAAE,EAAE,CAAC;IACjE,0DAA0D;IAC1D,gBAAgB,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IACpC,oEAAoE;IACpE,IAAI,EAAE,UAAU,EAAE,CAAC;IACnB,4DAA4D;IAC5D,mBAAmB,EAAE,kBAAkB,EAAE,CAAC;IAC1C;;;OAGG;IACH,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,eAAO,MAAM,OAAO,IAAI,CAAC;AAwBzB,wBAAgB,YAAY,CAC1B,OAAO,EAAE,YAAY,EAAE,EACvB,KAAK,GAAE,CAAC,YAAY,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,EAAO,GACnE,eAAe,CAiHjB;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,YAAY,GAAG,IAAI,CA8CnF"}
|