@trazum/core 1.86.0 → 2.1.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 +3 -3
- package/dist/config-schema.d.ts +10 -10
- package/dist/config-schema.d.ts.map +1 -1
- package/dist/config-schema.js +90 -36
- package/dist/config-schema.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/optimize.d.ts.map +1 -1
- package/dist/optimize.js +16 -1
- package/dist/optimize.js.map +1 -1
- package/dist/phrases.d.ts.map +1 -1
- package/dist/phrases.js +36 -0
- package/dist/phrases.js.map +1 -1
- package/dist/receipt.d.ts +33 -5
- package/dist/receipt.d.ts.map +1 -1
- package/dist/receipt.js +44 -18
- package/dist/receipt.js.map +1 -1
- package/dist/reprice.d.ts +74 -0
- package/dist/reprice.d.ts.map +1 -1
- package/dist/reprice.js +97 -20
- package/dist/reprice.js.map +1 -1
- package/dist/segment.d.ts.map +1 -1
- package/dist/segment.js +95 -1
- package/dist/segment.js.map +1 -1
- package/dist/structure.d.ts +27 -0
- package/dist/structure.d.ts.map +1 -1
- package/dist/structure.js +277 -9
- package/dist/structure.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/config-schema.ts +100 -44
- package/src/index.ts +8 -2
- package/src/optimize.ts +16 -1
- package/src/phrases.ts +36 -0
- package/src/receipt.ts +75 -18
- package/src/reprice.ts +164 -17
- package/src/segment.ts +95 -1
- package/src/structure.ts +275 -9
- package/src/types.ts +2 -0
package/src/config-schema.ts
CHANGED
|
@@ -314,48 +314,104 @@ export interface TrazumConfig {
|
|
|
314
314
|
pricing?: string;
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
+
/**
|
|
318
|
+
* A key list bound to the shape it describes.
|
|
319
|
+
*
|
|
320
|
+
* Every list below is what `rejectUnknownKeys` compares an incoming config
|
|
321
|
+
* against, and each one is a second copy of an interface declared elsewhere in
|
|
322
|
+
* this file. Nothing held them together. A plant added `telemetry?: boolean` to
|
|
323
|
+
* `TrazumConfig`, left `CONFIG_KEYS` alone, and **all 2,115 core tests passed**
|
|
324
|
+
* — leaving a documented setting the parser refuses by name, and the reverse
|
|
325
|
+
* plant leaving a key the parser accepts and nothing reads, which is the worse
|
|
326
|
+
* of the two because it is silent.
|
|
327
|
+
*
|
|
328
|
+
* `Record<keyof T, true>` closes both directions at compile time. The published
|
|
329
|
+
* arrays are derived from the maps rather than written again: key order is
|
|
330
|
+
* insertion order, so what a consumer reads is unchanged.
|
|
331
|
+
*/
|
|
332
|
+
const keyList = <T>(map: Record<keyof T, true>): readonly (keyof T & string)[] =>
|
|
333
|
+
Object.keys(map) as (keyof T & string)[];
|
|
334
|
+
|
|
317
335
|
/**
|
|
318
336
|
* Every key the config accepts. Exported so the CLI's help can be tested
|
|
319
337
|
* against it rather than against a second hand-maintained list — a setting the
|
|
320
338
|
* help never mentions is one only the changelog knows about.
|
|
321
339
|
*/
|
|
322
|
-
export const CONFIG_KEYS =
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
export const CONFIG_BASELINE_KEYS =
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
export const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
340
|
+
export const CONFIG_KEYS = keyList<TrazumConfig>({
|
|
341
|
+
level: true,
|
|
342
|
+
locale: true,
|
|
343
|
+
disable: true,
|
|
344
|
+
usage: true,
|
|
345
|
+
budgets: true,
|
|
346
|
+
labels: true,
|
|
347
|
+
spend: true,
|
|
348
|
+
limits: true,
|
|
349
|
+
sources: true,
|
|
350
|
+
store: true,
|
|
351
|
+
waive: true,
|
|
352
|
+
maxGrowth: true,
|
|
353
|
+
baseline: true,
|
|
354
|
+
extensions: true,
|
|
355
|
+
ignore: true,
|
|
356
|
+
pricing: true,
|
|
357
|
+
outcomes: true,
|
|
358
|
+
ladders: true,
|
|
359
|
+
owners: true,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
export const CONFIG_BASELINE_KEYS = keyList<BaselineConfig>({
|
|
363
|
+
path: true,
|
|
364
|
+
maxGrowthTokens: true,
|
|
365
|
+
maxGrowthPct: true,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
export const CONFIG_SPEND_KEYS = keyList<SpendConfig>({
|
|
369
|
+
maxUsd: true,
|
|
370
|
+
monthlyUsd: true,
|
|
371
|
+
maxDayUsd: true,
|
|
372
|
+
maxSessionUsd: true,
|
|
373
|
+
maxCacheLossUsd: true,
|
|
374
|
+
byLabel: true,
|
|
375
|
+
bySource: true,
|
|
376
|
+
substitute: true,
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
export const CONFIG_LIMITS_KEYS = keyList<LimitsConfig>({
|
|
380
|
+
dayUsd: true,
|
|
381
|
+
sessionUsd: true,
|
|
382
|
+
byLabel: true,
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
export const CONFIG_WAIVE_KEYS = keyList<WaiveEntry>({
|
|
386
|
+
gate: true,
|
|
387
|
+
reason: true,
|
|
388
|
+
until: true,
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
/*
|
|
392
|
+
`store` has no named interface, so the shape is reached through the field
|
|
393
|
+
itself. Naming a type purely to bind a list would put a third copy in the
|
|
394
|
+
file, which is the fault this is fixing.
|
|
395
|
+
*/
|
|
396
|
+
export const CONFIG_STORE_KEYS = keyList<NonNullable<TrazumConfig['store']>>({
|
|
397
|
+
keepDays: true,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
export const CONFIG_OUTCOME_KEYS = keyList<OutcomeVocabulary>({
|
|
401
|
+
values: true,
|
|
402
|
+
success: true,
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
export const CONFIG_LADDER_KEYS = keyList<LadderPolicy>({
|
|
406
|
+
tiers: true,
|
|
407
|
+
escalateOn: true,
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
export const CONFIG_OWNERS_KEYS = keyList<OwnersConfig>({
|
|
411
|
+
patterns: true,
|
|
412
|
+
shared: true,
|
|
413
|
+
budgets: true,
|
|
414
|
+
});
|
|
359
415
|
|
|
360
416
|
/**
|
|
361
417
|
* The gates a waiver can silence. The list is closed on purpose: a waiver
|
|
@@ -370,13 +426,13 @@ export const WAIVABLE_GATES = [
|
|
|
370
426
|
'maxGrowthUsd',
|
|
371
427
|
] as const;
|
|
372
428
|
|
|
373
|
-
export const CONFIG_USAGE_KEYS =
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
429
|
+
export const CONFIG_USAGE_KEYS = keyList<UsageProfile>({
|
|
430
|
+
model: true,
|
|
431
|
+
callsPerMonth: true,
|
|
432
|
+
avgOutputTokens: true,
|
|
433
|
+
cacheHitRate: true,
|
|
434
|
+
batchEligible: true,
|
|
435
|
+
});
|
|
380
436
|
|
|
381
437
|
const TOP_LEVEL_KEYS = CONFIG_KEYS;
|
|
382
438
|
const USAGE_KEYS = CONFIG_USAGE_KEYS;
|
package/src/index.ts
CHANGED
|
@@ -348,10 +348,16 @@ export type { RepeatedTurns, RepeatsOptions, RepeatsTracker } from './repeats.js
|
|
|
348
348
|
// See context-pressure.ts.
|
|
349
349
|
export { contextPressure } from './context-pressure.js';
|
|
350
350
|
export type { ContextPressure, ContextPressureOptions } from './context-pressure.js';
|
|
351
|
-
export { priceTokensOn, repriceProfile } from './reprice.js';
|
|
351
|
+
export { priceTokensOn, repriceProfile, repriceReceipt } from './reprice.js';
|
|
352
352
|
export { ownRate, switchAnalysis } from './switch.js';
|
|
353
353
|
export type { BreakEvenRefusal, SwitchAnalysis, SwitchOptions } from './switch.js';
|
|
354
|
-
export type {
|
|
354
|
+
export type {
|
|
355
|
+
OverContextSlice,
|
|
356
|
+
RepriceReport,
|
|
357
|
+
RepriceableInput,
|
|
358
|
+
RepriceableSlice,
|
|
359
|
+
RepricedSlice,
|
|
360
|
+
} from './reprice.js';
|
|
355
361
|
export { createSessionLedgerTracker, singleTurnCacheWrites } from './session-ledger.js';
|
|
356
362
|
// What one conversation costs — median and p95, exact. See session-cost.ts.
|
|
357
363
|
export { createSessionCostTracker, sessionCostShapes } from './session-cost.js';
|
package/src/optimize.ts
CHANGED
|
@@ -189,7 +189,22 @@ export function optimize(prompt: string, options: OptimizeOptions = {}): Optimiz
|
|
|
189
189
|
|
|
190
190
|
const ruleResults: RuleResult[] = [...byRule.values()];
|
|
191
191
|
|
|
192
|
-
|
|
192
|
+
/*
|
|
193
|
+
**Trimmed before the masks come off, not after.** `unmask(...).trim()` runs
|
|
194
|
+
on the reassembled string, where a protected span is ordinary text again —
|
|
195
|
+
so the trim could edit content every mask in this pipeline had just
|
|
196
|
+
promised to leave alone. A prompt whose first line is indented code lost
|
|
197
|
+
that indentation here, outside every guard, and the loss was invisible
|
|
198
|
+
because the trim is the last thing that happens.
|
|
199
|
+
|
|
200
|
+
It surfaced through idempotence rather than through a report: the block
|
|
201
|
+
stopped looking like a block, so the next pass no longer protected it and
|
|
202
|
+
the rules ate the code. The fuzzer found 54 of 1,500 corpus inputs that way.
|
|
203
|
+
|
|
204
|
+
Trimming the masked string cannot reach a protected span, because a
|
|
205
|
+
protected span is a placeholder at that point.
|
|
206
|
+
*/
|
|
207
|
+
const optimized = unmask(current.trim(), vault);
|
|
193
208
|
const tokensBefore = count(prompt);
|
|
194
209
|
const tokensAfter = count(optimized);
|
|
195
210
|
|
package/src/phrases.ts
CHANGED
|
@@ -248,6 +248,42 @@ export const FILLER: readonly string[] = [
|
|
|
248
248
|
"it's important to note that",
|
|
249
249
|
'it is worth noting that',
|
|
250
250
|
"it's worth noting that",
|
|
251
|
+
/*
|
|
252
|
+
The five below were missing while the same constructions sat in other
|
|
253
|
+
languages' sections of this very list, which is how they were found: not by
|
|
254
|
+
taste, but by reading across the sections and asking what English lacked.
|
|
255
|
+
|
|
256
|
+
it should be noted that <- de: 'es sei darauf hingewiesen, dass'
|
|
257
|
+
it is worth mentioning that <- es: 'vale la pena mencionar que'
|
|
258
|
+
it is important to mention <- es: 'es importante mencionar que'
|
|
259
|
+
|
|
260
|
+
English carried four entries in this family and **two** constructions:
|
|
261
|
+
`it is important to note that` and `it is worth noting that`, each written
|
|
262
|
+
twice for the contraction. Spanish carried five constructions in five
|
|
263
|
+
entries. So the English section was not short of entries — it was spending
|
|
264
|
+
them on contractions of the same two phrasings while another language
|
|
265
|
+
covered five different ones.
|
|
266
|
+
|
|
267
|
+
That distinction matters and was got wrong once. The first version of this
|
|
268
|
+
comment said the English section was *the thinnest in the file*, which is
|
|
269
|
+
false: counted per language it is the longest, 12 entries against Spanish's
|
|
270
|
+
12 and Italian's 9. The gap was inside one family, and generalising from it
|
|
271
|
+
to the whole list took a claim nobody had checked. The check is one script.
|
|
272
|
+
|
|
273
|
+
Measured on fifteen padded sentences an editor would trim without touching
|
|
274
|
+
an instruction, the rules recovered 34 of 76 tokens; these were among the
|
|
275
|
+
misses.
|
|
276
|
+
|
|
277
|
+
Nothing is added here for the other five languages. A phrasing they lack is
|
|
278
|
+
for whoever reads the language to judge — `docs/language-maintainer.md`
|
|
279
|
+
exists for exactly that, and guessing at French filler from an English ear
|
|
280
|
+
is how a rule starts editing prompts it does not understand.
|
|
281
|
+
*/
|
|
282
|
+
'it should be noted that',
|
|
283
|
+
'it is worth mentioning that',
|
|
284
|
+
"it's worth mentioning that",
|
|
285
|
+
'it is important to mention that',
|
|
286
|
+
"it's important to mention that",
|
|
251
287
|
'needless to say',
|
|
252
288
|
'in essence',
|
|
253
289
|
'at the end of the day',
|
package/src/receipt.ts
CHANGED
|
@@ -150,6 +150,34 @@ export interface ReceiptLine {
|
|
|
150
150
|
money: ReceiptMoney;
|
|
151
151
|
/** The catalogue's published rates, as provenance. */
|
|
152
152
|
pricing: ReceiptPricing;
|
|
153
|
+
/**
|
|
154
|
+
* The largest single call in this slice, cache reads and writes included.
|
|
155
|
+
*
|
|
156
|
+
* **The one number that says whether these calls would fit somewhere else.**
|
|
157
|
+
* A cheaper model with a smaller context window does not make a 400k-token
|
|
158
|
+
* call cheaper, it makes it impossible, and counting an impossible call's
|
|
159
|
+
* price difference as a saving is the flattering direction this repository
|
|
160
|
+
* refuses. The maximum rather than an average, for the same reason: one call
|
|
161
|
+
* over the ceiling is a failed call, and a mean hides it.
|
|
162
|
+
*
|
|
163
|
+
* Added in 2.1.0, and the gap it closes is worth naming. The two cache-write
|
|
164
|
+
* TTL fields above have always been here *so a consumer could reprice this
|
|
165
|
+
* traffic against another model's rates* -- and `repriceProfile` refuses to
|
|
166
|
+
* price traffic that would not fit, which no reader of a receipt could
|
|
167
|
+
* honour, because the receipt did not carry this. The format was built for
|
|
168
|
+
* repricing and stopped one field short of it.
|
|
169
|
+
*/
|
|
170
|
+
maxCallInputTokens: number;
|
|
171
|
+
/**
|
|
172
|
+
* Calls in this slice whose cache-write TTL the log did not state, so the
|
|
173
|
+
* cheaper 5-minute rate was assumed.
|
|
174
|
+
*
|
|
175
|
+
* The document already reports the organisation-wide figure as an
|
|
176
|
+
* `assumed-write-ttl` gap. This is the same fact per slice, which is the
|
|
177
|
+
* resolution a repricing needs: the assumption travels into the comparison,
|
|
178
|
+
* and a reader is entitled to know which slices carry it.
|
|
179
|
+
*/
|
|
180
|
+
assumedWriteTtlCalls: number;
|
|
153
181
|
}
|
|
154
182
|
|
|
155
183
|
/** Everything a receipt refuses to answer, typed rather than left implicit. */
|
|
@@ -220,26 +248,50 @@ export interface ReceiptDocument {
|
|
|
220
248
|
}
|
|
221
249
|
|
|
222
250
|
/**
|
|
223
|
-
* The fields that leave this machine,
|
|
251
|
+
* The fields that leave this machine, keyed by the interface they describe.
|
|
252
|
+
*
|
|
253
|
+
* `Record<keyof ReceiptLine, true>` is what holds the two together: a field
|
|
254
|
+
* added to `ReceiptLine` and not added here is a **compile error**, and so is
|
|
255
|
+
* an entry here that `ReceiptLine` does not have. A whitelist maintained in two
|
|
256
|
+
* places is a whitelist with a hole in it.
|
|
257
|
+
*
|
|
258
|
+
* **This used to say the redaction guard held that, and it did not.** The guard
|
|
259
|
+
* reads the emitted lines and checks every key it finds against this list,
|
|
260
|
+
* which catches an emitter that emits an unlisted field — a real property, kept
|
|
261
|
+
* below. It cannot catch a field added to the *type*: a plant added
|
|
262
|
+
* `operator?: string` to `ReceiptLine`, emitted it on no line the fixture
|
|
263
|
+
* built, and all twelve checks passed. A field populated in one branch is
|
|
264
|
+
* exactly the shape a leak takes, and the published whitelist customers read to
|
|
265
|
+
* see what leaves their machine would have been wrong while its own guard said
|
|
266
|
+
* otherwise.
|
|
267
|
+
*/
|
|
268
|
+
const RECEIPT_LINE_FIELD_MAP = {
|
|
269
|
+
label: true,
|
|
270
|
+
model: true,
|
|
271
|
+
calls: true,
|
|
272
|
+
inputTokens: true,
|
|
273
|
+
cacheReadTokens: true,
|
|
274
|
+
cacheWriteTokens: true,
|
|
275
|
+
cacheWrite5mTokens: true,
|
|
276
|
+
cacheWrite1hTokens: true,
|
|
277
|
+
outputTokens: true,
|
|
278
|
+
usd: true,
|
|
279
|
+
money: true,
|
|
280
|
+
pricing: true,
|
|
281
|
+
maxCallInputTokens: true,
|
|
282
|
+
assumedWriteTtlCalls: true,
|
|
283
|
+
} satisfies Record<keyof ReceiptLine, true>;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* The same list, published.
|
|
224
287
|
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
288
|
+
* Derived from the map rather than written again, because writing it twice is
|
|
289
|
+
* the fault this exists to prevent. Key order is insertion order, so the array
|
|
290
|
+
* a consumer reads is unchanged.
|
|
228
291
|
*/
|
|
229
|
-
export const RECEIPT_LINE_FIELDS = Object.freeze(
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
'calls',
|
|
233
|
-
'inputTokens',
|
|
234
|
-
'cacheReadTokens',
|
|
235
|
-
'cacheWriteTokens',
|
|
236
|
-
'cacheWrite5mTokens',
|
|
237
|
-
'cacheWrite1hTokens',
|
|
238
|
-
'outputTokens',
|
|
239
|
-
'usd',
|
|
240
|
-
'money',
|
|
241
|
-
'pricing',
|
|
242
|
-
] as const);
|
|
292
|
+
export const RECEIPT_LINE_FIELDS: readonly string[] = Object.freeze(
|
|
293
|
+
Object.keys(RECEIPT_LINE_FIELD_MAP),
|
|
294
|
+
);
|
|
243
295
|
|
|
244
296
|
const lineFrom = (
|
|
245
297
|
label: string,
|
|
@@ -282,6 +334,11 @@ const lineFrom = (
|
|
|
282
334
|
outputPerMTok: priced.outputPerMTok,
|
|
283
335
|
reviewedOn: PROVIDER_REVIEWED[priced.provider ?? ''] ?? null,
|
|
284
336
|
},
|
|
337
|
+
/* Copied, never derived. Neither can be recovered from the aggregates
|
|
338
|
+
above: a maximum is not a mean, and how many calls were silent about
|
|
339
|
+
their TTL is not visible in a token total. */
|
|
340
|
+
maxCallInputTokens: breakdown.maxCallInputTokens,
|
|
341
|
+
assumedWriteTtlCalls: breakdown.assumedWriteTtlCalls,
|
|
285
342
|
};
|
|
286
343
|
};
|
|
287
344
|
|
package/src/reprice.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { effectivePricing, multipliersFor } from './pricing.js';
|
|
2
2
|
import type { PricingCatalogue } from './pricing.js';
|
|
3
3
|
import type { ModelPricing } from './types.js';
|
|
4
|
+
import type { ReceiptDocument } from './receipt.js';
|
|
4
5
|
import type { UsageBreakdown, UsageProfileReport } from './usage.js';
|
|
6
|
+
import { UNLABELLED } from './usage.js';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* The same tokens at another model's rates.
|
|
@@ -195,11 +197,50 @@ export function priceTokensOn(
|
|
|
195
197
|
* against a price nobody has is worse than no comparison, and the caller is
|
|
196
198
|
* better placed to say so in its own words than this is to invent a zero.
|
|
197
199
|
*/
|
|
198
|
-
|
|
199
|
-
|
|
200
|
+
/**
|
|
201
|
+
* The least a slice has to carry to be repriced.
|
|
202
|
+
*
|
|
203
|
+
* Narrower than `UsageBreakdown` on purpose, and that narrowing is what lets a
|
|
204
|
+
* **receipt** be repriced at all. A receipt is the aggregate that leaves a
|
|
205
|
+
* machine; it does not carry a breakdown's truncation counts or its cache
|
|
206
|
+
* economics, and it never will, because those are answers to questions a
|
|
207
|
+
* receipt does not exist to answer. What it does carry — since 2.1.0 — is
|
|
208
|
+
* exactly this.
|
|
209
|
+
*
|
|
210
|
+
* Two entry points feed it and one implementation consumes it. Two copies of
|
|
211
|
+
* this loop, one reading profiles and one reading receipts, is the defect this
|
|
212
|
+
* codebase has found more times than any other, and it would be worse here
|
|
213
|
+
* than usual: the copies would drift on which traffic is refused, and a
|
|
214
|
+
* refusal that quietly stops happening reads as a saving.
|
|
215
|
+
*/
|
|
216
|
+
export interface RepriceableSlice {
|
|
217
|
+
/** `UNLABELLED` for the unlabelled bucket, never `null`. */
|
|
218
|
+
label: string;
|
|
219
|
+
model: string;
|
|
220
|
+
calls: number;
|
|
221
|
+
inputTokens: number;
|
|
222
|
+
cacheReadTokens: number;
|
|
223
|
+
cacheWrite5mTokens: number;
|
|
224
|
+
cacheWrite1hTokens: number;
|
|
225
|
+
outputTokens: number;
|
|
226
|
+
/** What it actually cost, as apportioned rather than as recomputed. */
|
|
227
|
+
totalUsd: number;
|
|
228
|
+
maxCallInputTokens: number;
|
|
229
|
+
assumedWriteTtlCalls: number;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** The slices, plus what could not be priced at all. */
|
|
233
|
+
export interface RepriceableInput {
|
|
234
|
+
slices: readonly RepriceableSlice[];
|
|
235
|
+
unpricedModels: readonly string[];
|
|
236
|
+
unpricedCalls: number;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function repriceSlices(
|
|
240
|
+
input: RepriceableInput,
|
|
200
241
|
targetId: string,
|
|
201
242
|
catalogue: PricingCatalogue,
|
|
202
|
-
on: Date
|
|
243
|
+
on: Date,
|
|
203
244
|
): RepriceReport | null {
|
|
204
245
|
const target = catalogue.byId.get(targetId);
|
|
205
246
|
if (!target) return null;
|
|
@@ -209,17 +250,16 @@ export function repriceProfile(
|
|
|
209
250
|
const alreadyOnTarget = { calls: 0, usd: 0 };
|
|
210
251
|
let assumedWriteTtlCalls = 0;
|
|
211
252
|
|
|
212
|
-
for (const
|
|
213
|
-
|
|
214
|
-
if (slice.model === target.id) {
|
|
253
|
+
for (const breakdown of input.slices) {
|
|
254
|
+
if (breakdown.model === target.id) {
|
|
215
255
|
alreadyOnTarget.calls += breakdown.calls;
|
|
216
256
|
alreadyOnTarget.usd += breakdown.totalUsd;
|
|
217
257
|
continue;
|
|
218
258
|
}
|
|
219
259
|
if (breakdown.maxCallInputTokens > target.contextWindow) {
|
|
220
260
|
overContext.push({
|
|
221
|
-
label:
|
|
222
|
-
model:
|
|
261
|
+
label: breakdown.label,
|
|
262
|
+
model: breakdown.model,
|
|
223
263
|
calls: breakdown.calls,
|
|
224
264
|
currentUsd: breakdown.totalUsd,
|
|
225
265
|
maxCallInputTokens: breakdown.maxCallInputTokens,
|
|
@@ -261,8 +301,8 @@ export function repriceProfile(
|
|
|
261
301
|
}
|
|
262
302
|
: null;
|
|
263
303
|
slices.push({
|
|
264
|
-
label:
|
|
265
|
-
model:
|
|
304
|
+
label: breakdown.label,
|
|
305
|
+
model: breakdown.model,
|
|
266
306
|
calls: breakdown.calls,
|
|
267
307
|
currentUsd: breakdown.totalUsd,
|
|
268
308
|
targetUsd,
|
|
@@ -287,15 +327,14 @@ export function repriceProfile(
|
|
|
287
327
|
let batchOnTarget: { targetUsd: number } | null = null;
|
|
288
328
|
if (batchRate !== null && batchRate < 1 && slices.length > 0) {
|
|
289
329
|
const { inputPerMTok, outputPerMTok } = effectivePricing(target, on);
|
|
290
|
-
const movable =
|
|
291
|
-
(slice) =>
|
|
292
|
-
slice.model !== target.id && slice.breakdown.maxCallInputTokens <= target.contextWindow,
|
|
330
|
+
const movable = input.slices.filter(
|
|
331
|
+
(slice) => slice.model !== target.id && slice.maxCallInputTokens <= target.contextWindow,
|
|
293
332
|
);
|
|
294
333
|
const inOutUsd = movable.reduce(
|
|
295
334
|
(sum, slice) =>
|
|
296
335
|
sum +
|
|
297
|
-
(slice.
|
|
298
|
-
(slice.
|
|
336
|
+
(slice.inputTokens / 1_000_000) * inputPerMTok +
|
|
337
|
+
(slice.outputTokens / 1_000_000) * outputPerMTok,
|
|
299
338
|
0,
|
|
300
339
|
);
|
|
301
340
|
batchOnTarget = { targetUsd: targetUsd - inOutUsd * (1 - batchRate) };
|
|
@@ -310,9 +349,117 @@ export function repriceProfile(
|
|
|
310
349
|
overContext,
|
|
311
350
|
alreadyOnTarget,
|
|
312
351
|
assumedWriteTtlCalls,
|
|
313
|
-
|
|
314
|
-
|
|
352
|
+
/* Copied rather than passed through. The input's array belongs to the
|
|
353
|
+
caller -- a profile's own field, or a receipt's gap -- and handing it out
|
|
354
|
+
inside a report is how a reader ends up able to edit the document they
|
|
355
|
+
were given. */
|
|
356
|
+
unpricedModels: [...input.unpricedModels],
|
|
357
|
+
unpricedCalls: input.unpricedCalls,
|
|
315
358
|
batchOnTarget,
|
|
316
359
|
sameTokensAssumed: true,
|
|
317
360
|
};
|
|
318
361
|
}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Reprices a profile's label-and-model slices onto one target model.
|
|
366
|
+
*
|
|
367
|
+
* Returns `null` when the catalogue does not know the target: a comparison
|
|
368
|
+
* against a price nobody has is worse than no comparison, and the caller is
|
|
369
|
+
* better placed to say so in its own words than this is to invent a zero.
|
|
370
|
+
*/
|
|
371
|
+
export function repriceProfile(
|
|
372
|
+
report: UsageProfileReport,
|
|
373
|
+
targetId: string,
|
|
374
|
+
catalogue: PricingCatalogue,
|
|
375
|
+
on: Date = new Date(),
|
|
376
|
+
): RepriceReport | null {
|
|
377
|
+
return repriceSlices(
|
|
378
|
+
{
|
|
379
|
+
slices: report.byLabelAndModel.map((slice) => ({
|
|
380
|
+
label: slice.label,
|
|
381
|
+
model: slice.model,
|
|
382
|
+
calls: slice.breakdown.calls,
|
|
383
|
+
inputTokens: slice.breakdown.inputTokens,
|
|
384
|
+
cacheReadTokens: slice.breakdown.cacheReadTokens,
|
|
385
|
+
cacheWrite5mTokens: slice.breakdown.cacheWrite5mTokens,
|
|
386
|
+
cacheWrite1hTokens: slice.breakdown.cacheWrite1hTokens,
|
|
387
|
+
outputTokens: slice.breakdown.outputTokens,
|
|
388
|
+
totalUsd: slice.breakdown.totalUsd,
|
|
389
|
+
maxCallInputTokens: slice.breakdown.maxCallInputTokens,
|
|
390
|
+
assumedWriteTtlCalls: slice.breakdown.assumedWriteTtlCalls,
|
|
391
|
+
})),
|
|
392
|
+
unpricedModels: report.unpricedModels,
|
|
393
|
+
unpricedCalls: report.unpriced.calls,
|
|
394
|
+
},
|
|
395
|
+
targetId,
|
|
396
|
+
catalogue,
|
|
397
|
+
on,
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* The same question, asked of a **receipt** rather than of the log behind it.
|
|
403
|
+
*
|
|
404
|
+
* ## Why this exists
|
|
405
|
+
*
|
|
406
|
+
* A receipt is what leaves a machine, and by the time anybody wants to ask
|
|
407
|
+
* *what would these calls have cost on the small model* the log is usually
|
|
408
|
+
* gone — deleted by a retention policy, on a runner that no longer exists, or
|
|
409
|
+
* simply on somebody else's laptop. A comparison that can only be made where
|
|
410
|
+
* the log still is can only be made by the person who least needs it.
|
|
411
|
+
*
|
|
412
|
+
* It also settles a question the receipt format had left open. The two
|
|
413
|
+
* cache-write TTL fields say in their own comment that they exist *so a
|
|
414
|
+
* consumer can reprice this traffic*, and until 2.1.0 no consumer could: the
|
|
415
|
+
* refusal that protects the whole comparison — never price traffic the target
|
|
416
|
+
* could not have accepted — needs the largest call, and the receipt did not
|
|
417
|
+
* carry it. This is the entry point that field was added for.
|
|
418
|
+
*
|
|
419
|
+
* ## What it refuses, which is the same four things
|
|
420
|
+
*
|
|
421
|
+
* Nothing here is a second implementation. It maps a receipt's lines onto the
|
|
422
|
+
* same slices `repriceProfile` maps a profile onto, and the one loop that
|
|
423
|
+
* refuses over-context traffic, excludes calls already on the target, keeps
|
|
424
|
+
* the write TTLs apart and states `sameTokensAssumed` is the loop both use.
|
|
425
|
+
*
|
|
426
|
+
* The unlabelled bucket travels as `UNLABELLED` rather than `null`, which is
|
|
427
|
+
* the spelling every other consumer of these types already reads.
|
|
428
|
+
*/
|
|
429
|
+
export function repriceReceipt(
|
|
430
|
+
document: ReceiptDocument,
|
|
431
|
+
targetId: string,
|
|
432
|
+
catalogue: PricingCatalogue,
|
|
433
|
+
on: Date = new Date(),
|
|
434
|
+
): RepriceReport | null {
|
|
435
|
+
/*
|
|
436
|
+
* What the catalogue could not price is in the document's gaps rather than
|
|
437
|
+
* in a line, which is the receipt's own arrangement: an unpriced call has no
|
|
438
|
+
* rate, so it has no line to be in. Read from there rather than left at
|
|
439
|
+
* zero, so a comparison covering half a bill cannot look complete.
|
|
440
|
+
*/
|
|
441
|
+
const unpriced = document.gaps.find((gap) => gap.kind === 'unpriced');
|
|
442
|
+
|
|
443
|
+
return repriceSlices(
|
|
444
|
+
{
|
|
445
|
+
slices: document.lines.map((line) => ({
|
|
446
|
+
label: line.label ?? UNLABELLED,
|
|
447
|
+
model: line.model,
|
|
448
|
+
calls: line.calls,
|
|
449
|
+
inputTokens: line.inputTokens,
|
|
450
|
+
cacheReadTokens: line.cacheReadTokens,
|
|
451
|
+
cacheWrite5mTokens: line.cacheWrite5mTokens,
|
|
452
|
+
cacheWrite1hTokens: line.cacheWrite1hTokens,
|
|
453
|
+
outputTokens: line.outputTokens,
|
|
454
|
+
totalUsd: line.usd,
|
|
455
|
+
maxCallInputTokens: line.maxCallInputTokens,
|
|
456
|
+
assumedWriteTtlCalls: line.assumedWriteTtlCalls,
|
|
457
|
+
})),
|
|
458
|
+
unpricedModels: unpriced === undefined ? [] : unpriced.models,
|
|
459
|
+
unpricedCalls: unpriced === undefined ? 0 : unpriced.calls,
|
|
460
|
+
},
|
|
461
|
+
targetId,
|
|
462
|
+
catalogue,
|
|
463
|
+
on,
|
|
464
|
+
);
|
|
465
|
+
}
|