pi-background-tasks 0.7.6 → 0.7.7

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.
@@ -1,4 +1,18 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import {
3
+ TOKEN_BUDGET_AFFINE_F_TOKENS,
4
+ TOKEN_BUDGET_CALIBRATION_VERSION,
5
+ TOKEN_BUDGET_FAMILY_CALIBRATIONS,
6
+ TOKEN_BUDGET_LARGE_PROMPT_MIN_BYTES,
7
+ TOKEN_BUDGET_RATE_SCALE,
8
+ estimateInputTokens,
9
+ knownTextSegment,
10
+ maxKnownTextBytesForTokens,
11
+ resolveTokenBudgetFamily,
12
+ unknownOutputContractSegment,
13
+ allowedInputTokens,
14
+ isUsableContextWindow,
15
+ } from '../context/token-budget.js';
2
16
  import {
3
17
  FUSION_CANDIDATE_SYSTEM_PROMPT,
4
18
  FUSION_EVALUATION_REPAIR_SYSTEM_PROMPT,
@@ -14,36 +28,59 @@ import {
14
28
  } from './prompts.js';
15
29
  import {
16
30
  FUSION_BUDGET_PLAN_SCHEMA_VERSION,
31
+ FUSION_CALIBRATION_VIOLATION_SCHEMA_VERSION,
17
32
  FUSION_EVALUATION_SCHEMA_VERSION,
18
33
  FusionError,
19
34
  type FusionBudgetBlocker,
35
+ type FusionBudgetCheckKind,
36
+ type FusionBudgetComponentBreakdown,
37
+ type FusionBudgetCounterfactuals,
20
38
  type FusionBudgetEmptyRequestVerdict,
21
39
  type FusionBudgetErrorDetail,
22
40
  type FusionBudgetPlanV1,
23
41
  type FusionBudgetPolicyDescriptor,
42
+ type FusionBudgetRouteTableEntry,
24
43
  type FusionBudgetStage,
25
44
  type FusionBudgetStageComposition,
26
45
  type FusionBudgetWarning,
46
+ type FusionCalibrationViolation,
27
47
  type FusionCanonicalInputV3,
28
48
  type FusionEvaluationV1,
29
49
  type FusionRouteCapacity,
30
50
  type FusionStage,
31
51
  type FusionStageBudgetPlanEntry,
52
+ type FusionChildRunResult,
32
53
  type ResolvedFusionModel,
33
54
  type ResolvedFusionModels,
34
55
  } from './types.js';
35
56
 
36
- export const FUSION_BYTES_PER_TOKEN_DIVISOR = 2;
57
+ export const FUSION_CALIBRATED_BYTES_PER_TOKEN = TOKEN_BUDGET_FAMILY_CALIBRATIONS;
37
58
 
38
59
  export const FUSION_CANDIDATE_MAX_OUTPUT_BYTES = 48 * 1024;
39
60
  export const FUSION_EVALUATION_MAX_OUTPUT_BYTES = 64 * 1024;
40
61
  export const FUSION_MERGE_MAX_OUTPUT_BYTES = 64 * 1024;
41
62
  export const FUSION_DIAGNOSTICS_MAX_BYTES = 8 * 1024;
42
63
 
43
- export const FUSION_RESERVED_OUTPUT_TOKENS = Math.ceil(
44
- FUSION_MERGE_MAX_OUTPUT_BYTES / FUSION_BYTES_PER_TOKEN_DIVISOR,
64
+ const FUSION_OUTPUT_RESERVE_RATE_X100 = 200;
65
+ export const FUSION_UTILIZATION_WARNING_THRESHOLD_BASIS_POINTS = 8000;
66
+ const BASIS_POINTS_DENOMINATOR = 10_000;
67
+
68
+ function ceilDiv(numerator: number, denominator: number): number {
69
+ if (!Number.isSafeInteger(numerator) || numerator < 0) {
70
+ throw new TypeError('ceilDiv numerator must be a non-negative safe integer');
71
+ }
72
+ if (!Number.isSafeInteger(denominator) || denominator <= 0) {
73
+ throw new TypeError('ceilDiv denominator must be a positive safe integer');
74
+ }
75
+ if (numerator === 0) return 0;
76
+ return Math.floor((numerator - 1) / denominator) + 1;
77
+ }
78
+
79
+ export const FUSION_RESERVED_OUTPUT_TOKENS = ceilDiv(
80
+ FUSION_MERGE_MAX_OUTPUT_BYTES * TOKEN_BUDGET_RATE_SCALE,
81
+ FUSION_OUTPUT_RESERVE_RATE_X100,
45
82
  );
46
- export const FUSION_FRAMING_RESERVE_TOKENS = 4_096;
83
+ export const FUSION_FRAMING_RESERVE_TOKENS = 0;
47
84
  export const FUSION_SAFETY_RESERVE_TOKENS = 4_096;
48
85
  export const FUSION_MIN_CANONICAL_INPUT_TOKENS = 8_192;
49
86
  export const FUSION_MIN_CONTEXT_WINDOW_TOKENS =
@@ -51,11 +88,11 @@ export const FUSION_MIN_CONTEXT_WINDOW_TOKENS =
51
88
  FUSION_RESERVED_OUTPUT_TOKENS +
52
89
  FUSION_FRAMING_RESERVE_TOKENS +
53
90
  FUSION_SAFETY_RESERVE_TOKENS;
54
- export const FUSION_UTILIZATION_WARNING_THRESHOLD = 0.8;
55
91
 
56
92
  export const FUSION_BUDGET_POLICY: FusionBudgetPolicyDescriptor = {
57
- id: 'fusion-budget-policy-v2',
58
- bytes_per_token_divisor: FUSION_BYTES_PER_TOKEN_DIVISOR,
93
+ id: 'fusion-budget-policy-v3',
94
+ calibration_version: TOKEN_BUDGET_CALIBRATION_VERSION,
95
+ calibration_table: FUSION_CALIBRATED_BYTES_PER_TOKEN,
59
96
  reserved_output_tokens: FUSION_RESERVED_OUTPUT_TOKENS,
60
97
  framing_reserve_tokens: FUSION_FRAMING_RESERVE_TOKENS,
61
98
  safety_reserve_tokens: FUSION_SAFETY_RESERVE_TOKENS,
@@ -63,7 +100,7 @@ export const FUSION_BUDGET_POLICY: FusionBudgetPolicyDescriptor = {
63
100
  evaluation_output_contract_bytes: FUSION_EVALUATION_MAX_OUTPUT_BYTES,
64
101
  merge_output_contract_bytes: FUSION_MERGE_MAX_OUTPUT_BYTES,
65
102
  diagnostics_contract_bytes: FUSION_DIAGNOSTICS_MAX_BYTES,
66
- utilization_warning_threshold: FUSION_UTILIZATION_WARNING_THRESHOLD,
103
+ utilization_warning_threshold_basis_points: FUSION_UTILIZATION_WARNING_THRESHOLD_BASIS_POINTS,
67
104
  };
68
105
 
69
106
  const EMPTY_REMEDIATION: readonly string[] = Object.freeze([
@@ -78,6 +115,23 @@ const REQUEST_REMEDIATION: readonly string[] = Object.freeze([
78
115
  "Raise the route's context window with a larger-context model via /fusion-models.",
79
116
  ]);
80
117
 
118
+ const RESERVATION_REMEDIATION: readonly string[] = Object.freeze([
119
+ 'Route the blocking stage to a model with larger byte capacity.',
120
+ 'Keep producer output contracts intact; do not shrink or truncate child answers.',
121
+ 'Inspect budget-plan.json for the advisory reservation component before retrying.',
122
+ ]);
123
+
124
+ const DENSE_REMEDIATION: readonly string[] = Object.freeze([
125
+ 'Remove or externalize low-whitespace dense ASCII payloads (base64, minified code, PEM/hex blocks), then retry.',
126
+ 'The prompt is preserved; no content was clipped to fit.',
127
+ ]);
128
+
129
+ const MULTIBYTE_REMEDIATION: readonly string[] = Object.freeze([
130
+ 'This rejection is dominated by multibyte UTF-8 content; use a larger-context route or split the non-Latin/CJK-heavy task.',
131
+ 'The budget plan includes the stricter multibyte advisory ceiling separately from the fatal estimate.',
132
+ 'The prompt is preserved; no content was clipped to fit.',
133
+ ]);
134
+
81
135
  const EMPTY_CANDIDATES: readonly [
82
136
  AnonymousFusionCandidate,
83
137
  AnonymousFusionCandidate,
@@ -140,7 +194,11 @@ interface StageForecastDraft {
140
194
  }
141
195
 
142
196
  export function fusionTokenUpperBound(utf8Bytes: number): number {
143
- return Math.ceil(utf8Bytes / FUSION_BYTES_PER_TOKEN_DIVISOR);
197
+ return estimateInputTokens({
198
+ family: 'unknown',
199
+ scope: 'conservative',
200
+ segments: [{ kind: 'known_text', bytes: utf8Bytes, multibyteBytes: 0, denseBytes: 0 }],
201
+ }).tokens;
144
202
  }
145
203
 
146
204
  export function fusionOutputContractBytes(stage: FusionStage): number {
@@ -169,7 +227,7 @@ function sha256Hex(value: string): string {
169
227
 
170
228
  function requirePositiveContextWindow(model: ResolvedFusionModel, role: string): number {
171
229
  const value = model.contextWindow;
172
- if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
230
+ if (!isUsableContextWindow(value)) {
173
231
  throw new FusionError(
174
232
  `fusion ${role} route ${model.qualifiedId} has no usable context window capacity`,
175
233
  { code: 'model_capacity_unknown', childCreated: false },
@@ -178,22 +236,45 @@ function requirePositiveContextWindow(model: ResolvedFusionModel, role: string):
178
236
  return value;
179
237
  }
180
238
 
239
+ function formatRateX100(value: number): string {
240
+ const whole = Math.floor(value / TOKEN_BUDGET_RATE_SCALE);
241
+ const frac = String(value % TOKEN_BUDGET_RATE_SCALE).padStart(2, '0');
242
+ return `${String(whole)}.${frac}`;
243
+ }
244
+
181
245
  function routeCapacity(
182
246
  model: ResolvedFusionModel,
183
247
  role: FusionRouteCapacity['role'],
184
248
  ): FusionRouteCapacity {
185
249
  const contextWindow = requirePositiveContextWindow(model, role);
186
- const allowed =
187
- contextWindow -
188
- FUSION_RESERVED_OUTPUT_TOKENS -
189
- FUSION_FRAMING_RESERVE_TOKENS -
190
- FUSION_SAFETY_RESERVE_TOKENS;
250
+ const allowed = allowedInputTokens(contextWindow, {
251
+ reservedOutputTokens: FUSION_RESERVED_OUTPUT_TOKENS,
252
+ framingReserveTokens: FUSION_FRAMING_RESERVE_TOKENS,
253
+ safetyReserveTokens: FUSION_SAFETY_RESERVE_TOKENS,
254
+ });
191
255
  if (allowed < FUSION_MIN_CANONICAL_INPUT_TOKENS) {
192
256
  throw new FusionError(
193
257
  `fusion ${role} route ${model.qualifiedId} has a ${String(contextWindow)}-token context window, but Fusion requires at least ${String(FUSION_MIN_CONTEXT_WINDOW_TOKENS)} tokens per configured route: ${String(FUSION_RESERVED_OUTPUT_TOKENS)} output + ${String(FUSION_FRAMING_RESERVE_TOKENS)} framing + ${String(FUSION_SAFETY_RESERVE_TOKENS)} safety + ${String(FUSION_MIN_CANONICAL_INPUT_TOKENS)} usable input. Choose a larger-context model for this slot with /fusion-models.`,
194
258
  { code: 'model_capacity_unknown', childCreated: false },
195
259
  );
196
260
  }
261
+ const family = resolveTokenBudgetFamily({ provider: model.provider, model: model.model });
262
+ const rateSource = estimateInputTokens({
263
+ family: family.family,
264
+ calibrationBacked: family.backed,
265
+ familyResolution: family.resolution,
266
+ allowedInputTokens: allowed,
267
+ scope: 'fusion',
268
+ segments: [
269
+ {
270
+ kind: 'known_text',
271
+ bytes: TOKEN_BUDGET_LARGE_PROMPT_MIN_BYTES,
272
+ multibyteBytes: 0,
273
+ denseBytes: 0,
274
+ asciiWhitespaceBytes: TOKEN_BUDGET_LARGE_PROMPT_MIN_BYTES,
275
+ },
276
+ ],
277
+ }).rateSource;
197
278
  return {
198
279
  role,
199
280
  provider: model.provider,
@@ -204,6 +285,11 @@ function routeCapacity(
204
285
  framing_reserve_tokens: FUSION_FRAMING_RESERVE_TOKENS,
205
286
  safety_reserve_tokens: FUSION_SAFETY_RESERVE_TOKENS,
206
287
  allowed_input_tokens: allowed,
288
+ family: family.family,
289
+ rate_source: rateSource,
290
+ byte_capacity_utf8_bytes: Math.floor(
291
+ (allowed * rateSource.effective_rate_bytes_per_token_x100) / TOKEN_BUDGET_RATE_SCALE,
292
+ ),
207
293
  };
208
294
  }
209
295
 
@@ -222,7 +308,12 @@ export function fusionLimitingRoute(
222
308
  ): FusionRouteCapacity {
223
309
  let limiting: FusionRouteCapacity | undefined;
224
310
  for (const route of routes) {
225
- if (limiting === undefined || route.allowed_input_tokens < limiting.allowed_input_tokens) {
311
+ if (
312
+ limiting === undefined ||
313
+ route.byte_capacity_utf8_bytes < limiting.byte_capacity_utf8_bytes ||
314
+ (route.byte_capacity_utf8_bytes === limiting.byte_capacity_utf8_bytes &&
315
+ route.role.localeCompare(limiting.role) < 0)
316
+ ) {
226
317
  limiting = route;
227
318
  }
228
319
  }
@@ -255,29 +346,80 @@ function candidateRole(slot: 1 | 2 | 3): FusionRouteCapacity['role'] {
255
346
  return 'candidate-3';
256
347
  }
257
348
 
349
+ function estimateRouteInput(
350
+ route: FusionRouteCapacity,
351
+ segments: Parameters<typeof estimateInputTokens>[0]['segments'],
352
+ ) {
353
+ return estimateInputTokens({
354
+ family: route.family,
355
+ calibrationBacked: route.rate_source.backed,
356
+ familyResolution: route.rate_source.model_resolution,
357
+ allowedInputTokens: route.allowed_input_tokens,
358
+ scope: 'fusion',
359
+ segments,
360
+ });
361
+ }
362
+
363
+ function utilizationBasisPoints(tokens: number, allowed: number): number {
364
+ return ceilDiv(tokens * BASIS_POINTS_DENOMINATOR, allowed);
365
+ }
366
+
258
367
  function forecastEntry(draft: StageForecastDraft): FusionStageBudgetPlanEntry {
259
- const forecastUtf8Bytes =
260
- utf8Bytes(draft.system_prompt) +
261
- utf8Bytes(draft.empty_user_prompt) +
262
- draft.upstream_output_contract_bytes;
263
- const tokens = fusionTokenUpperBound(forecastUtf8Bytes);
368
+ const inputSegments = [knownTextSegment(draft.system_prompt), knownTextSegment(draft.empty_user_prompt)];
369
+ const inputBytes = inputSegments.reduce((sum, segment) => sum + segment.bytes, 0);
370
+ const inputOnly = estimateRouteInput(draft.route, inputSegments);
371
+ const reservationSegments =
372
+ draft.upstream_output_contract_bytes === 0
373
+ ? inputSegments
374
+ : [...inputSegments, unknownOutputContractSegment(draft.upstream_output_contract_bytes)];
375
+ const reservation = estimateRouteInput(draft.route, reservationSegments);
376
+ const forecastUtf8Bytes = inputBytes + draft.upstream_output_contract_bytes;
264
377
  const entry: FusionStageBudgetPlanEntry = {
265
378
  budget_stage: draft.budget_stage,
266
379
  route: draft.route,
267
380
  conditional: draft.conditional,
381
+ check_kind: 'input_only_preflight',
382
+ input_utf8_bytes: inputBytes,
383
+ upstream_output_contract_bytes: draft.upstream_output_contract_bytes,
268
384
  forecast_utf8_bytes: forecastUtf8Bytes,
269
- forecast_input_tokens_upper_bound: tokens,
385
+ input_only_input_tokens_upper_bound: inputOnly.tokens,
386
+ forecast_input_tokens_upper_bound: reservation.tokens,
270
387
  allowed_input_tokens: draft.route.allowed_input_tokens,
271
- signed_headroom_tokens: draft.route.allowed_input_tokens - tokens,
272
- utilization: tokens / draft.route.allowed_input_tokens,
273
- fits: tokens <= draft.route.allowed_input_tokens,
388
+ input_only_signed_headroom_tokens: draft.route.allowed_input_tokens - inputOnly.tokens,
389
+ signed_headroom_tokens: draft.route.allowed_input_tokens - reservation.tokens,
390
+ input_only_utilization_basis_points: utilizationBasisPoints(
391
+ inputOnly.tokens,
392
+ draft.route.allowed_input_tokens,
393
+ ),
394
+ utilization_basis_points: utilizationBasisPoints(
395
+ reservation.tokens,
396
+ draft.route.allowed_input_tokens,
397
+ ),
398
+ input_only_estimate: inputOnly,
399
+ reservation_estimate: reservation,
400
+ fits: inputOnly.tokens <= draft.route.allowed_input_tokens,
401
+ reservation_fits: reservation.tokens <= draft.route.allowed_input_tokens,
274
402
  };
275
403
  if (draft.slot !== undefined) entry.slot = draft.slot;
276
404
  return entry;
277
405
  }
278
406
 
407
+ function maxKnownTextBytes(route: FusionRouteCapacity): number {
408
+ return maxKnownTextBytesForTokens({
409
+ family: route.family,
410
+ calibrationBacked: route.rate_source.backed,
411
+ familyResolution: route.rate_source.model_resolution,
412
+ allowedInputTokens: route.allowed_input_tokens,
413
+ scope: 'fusion',
414
+ });
415
+ }
416
+
279
417
  function blockerFromEntry(entry: FusionStageBudgetPlanEntry): FusionBudgetBlocker {
280
- return { ...entry, overage_tokens: Math.max(0, -entry.signed_headroom_tokens) };
418
+ return {
419
+ ...entry,
420
+ overage_tokens: Math.max(0, entry.input_only_input_tokens_upper_bound - entry.allowed_input_tokens),
421
+ bytes_over: Math.max(0, entry.input_utf8_bytes - maxKnownTextBytes(entry.route)),
422
+ };
281
423
  }
282
424
 
283
425
  function blockerOrder(blocker: FusionBudgetBlocker): number {
@@ -316,7 +458,7 @@ function replaceRequestText(input: FusionCanonicalInputV3, text: string): Fusion
316
458
  function visibleTextBytes(input: FusionCanonicalInputV3): number {
317
459
  let total = 0;
318
460
  for (const entry of input.conversation_projection.entries) {
319
- if (entry.kind === 'text') total += utf8Bytes(JSON.stringify(entry.text));
461
+ if (entry[0] === 't') total += utf8Bytes(JSON.stringify(entry[4]));
320
462
  }
321
463
  return total;
322
464
  }
@@ -324,19 +466,35 @@ function visibleTextBytes(input: FusionCanonicalInputV3): number {
324
466
  function omissionReceiptBytes(input: FusionCanonicalInputV3): number {
325
467
  let total = 0;
326
468
  for (const entry of input.conversation_projection.entries) {
327
- if (entry.kind === 'omitted_activity') total += utf8Bytes(JSON.stringify(entry));
469
+ if (entry[0] === 'o') total += utf8Bytes(JSON.stringify(entry));
328
470
  }
329
471
  return total;
330
472
  }
331
473
 
332
- function warningFromEntry(entry: FusionStageBudgetPlanEntry): FusionBudgetWarning {
333
- return { ...entry, threshold: FUSION_UTILIZATION_WARNING_THRESHOLD };
474
+ function warningFromEntry(entry: FusionStageBudgetPlanEntry): FusionBudgetWarning | undefined {
475
+ if (!entry.fits) return undefined;
476
+ if (!entry.reservation_fits) {
477
+ return {
478
+ ...entry,
479
+ warning_kind: 'worst_case_reservation',
480
+ threshold_basis_points: FUSION_UTILIZATION_WARNING_THRESHOLD_BASIS_POINTS,
481
+ };
482
+ }
483
+ if (entry.utilization_basis_points >= FUSION_UTILIZATION_WARNING_THRESHOLD_BASIS_POINTS) {
484
+ return {
485
+ ...entry,
486
+ warning_kind: 'input_utilization',
487
+ threshold_basis_points: FUSION_UTILIZATION_WARNING_THRESHOLD_BASIS_POINTS,
488
+ };
489
+ }
490
+ return undefined;
334
491
  }
335
492
 
336
493
  function warningsFor(entries: readonly FusionStageBudgetPlanEntry[]): readonly FusionBudgetWarning[] {
337
- return entries
338
- .filter((entry) => entry.fits && entry.utilization >= FUSION_UTILIZATION_WARNING_THRESHOLD)
339
- .map(warningFromEntry);
494
+ return entries.flatMap((entry) => {
495
+ const warning = warningFromEntry(entry);
496
+ return warning === undefined ? [] : [warning];
497
+ });
340
498
  }
341
499
 
342
500
  function entryLabel(entry: FusionStageBudgetPlanEntry): string {
@@ -348,7 +506,7 @@ function entryLabel(entry: FusionStageBudgetPlanEntry): string {
348
506
  function formatTable(entries: readonly FusionStageBudgetPlanEntry[]): string {
349
507
  const lines = entries.map(
350
508
  (entry) =>
351
- `${entryLabel(entry)} | route=${entry.route.qualified_id} | forecast=${String(entry.forecast_input_tokens_upper_bound)} | allowed=${String(entry.allowed_input_tokens)} | headroom=${String(entry.signed_headroom_tokens)} | ${entry.fits ? 'fits' : 'over'}`,
509
+ `${entryLabel(entry)} | route=${entry.route.qualified_id} | input=${String(entry.input_only_input_tokens_upper_bound)} | reserved=${String(entry.forecast_input_tokens_upper_bound)} | allowed=${String(entry.allowed_input_tokens)} | input_headroom=${String(entry.input_only_signed_headroom_tokens)} | reservation_headroom=${String(entry.signed_headroom_tokens)} | ${entry.fits ? 'input-fits' : 'input-over'} | ${entry.reservation_fits ? 'reservation-fits' : 'reservation-warn'}`,
352
510
  );
353
511
  return lines.join('\n');
354
512
  }
@@ -364,6 +522,27 @@ function formatComposition(composition: FusionBudgetStageComposition): string {
364
522
  ].join('; ');
365
523
  }
366
524
 
525
+ function dominantRemediation(
526
+ verdict: FusionBudgetEmptyRequestVerdict,
527
+ composition: FusionBudgetStageComposition | undefined,
528
+ dominantByteClass: string,
529
+ ): readonly string[] {
530
+ if (dominantByteClass === 'dense_ascii') return DENSE_REMEDIATION;
531
+ if (dominantByteClass === 'multibyte') return MULTIBYTE_REMEDIATION;
532
+ if (composition === undefined) return remediationFor(verdict);
533
+ const entries = [
534
+ { name: 'visible', bytes: composition.visible_text_bytes },
535
+ { name: 'request', bytes: composition.request_bytes },
536
+ { name: 'reservation', bytes: composition.upstream_output_contract_bytes },
537
+ ].sort((left, right) => right.bytes - left.bytes);
538
+ const dominant = entries[0];
539
+ if (dominant?.name === 'reservation') return RESERVATION_REMEDIATION;
540
+ if (dominant?.name === 'request' && !verdict.still_fails_with_empty_request) {
541
+ return REQUEST_REMEDIATION;
542
+ }
543
+ return EMPTY_REMEDIATION;
544
+ }
545
+
367
546
  function remediationFor(verdict: FusionBudgetEmptyRequestVerdict): readonly string[] {
368
547
  return verdict.still_fails_with_empty_request ? EMPTY_REMEDIATION : REQUEST_REMEDIATION;
369
548
  }
@@ -384,6 +563,116 @@ function stageFromBudgetStage(stage: FusionBudgetStage): FusionStage {
384
563
  return 'evaluation';
385
564
  }
386
565
 
566
+ function routeTable(routes: readonly FusionRouteCapacity[]): readonly FusionBudgetRouteTableEntry[] {
567
+ return [...routes]
568
+ .sort((left, right) => {
569
+ const byCapacity = left.byte_capacity_utf8_bytes - right.byte_capacity_utf8_bytes;
570
+ if (byCapacity !== 0) return byCapacity;
571
+ return left.role.localeCompare(right.role);
572
+ })
573
+ .map((route) => ({
574
+ role: route.role,
575
+ qualified_id: route.qualified_id,
576
+ allowed_input_tokens: route.allowed_input_tokens,
577
+ family: route.family,
578
+ effective_rate_bytes_per_token_x100: route.rate_source.effective_rate_bytes_per_token_x100,
579
+ byte_capacity_utf8_bytes: route.byte_capacity_utf8_bytes,
580
+ backed: route.rate_source.backed,
581
+ }));
582
+ }
583
+
584
+ function segmentTokensForBytes(route: FusionRouteCapacity, bytes: number): number {
585
+ if (bytes <= 0) return 0;
586
+ const estimate = estimateRouteInput(route, [
587
+ { kind: 'known_text', bytes, multibyteBytes: 0, denseBytes: 0 },
588
+ ]);
589
+ const segment = estimate.perSegment[0];
590
+ return segment === undefined ? 0 : segment.tokens;
591
+ }
592
+
593
+ function contractTokens(bytes: number): number {
594
+ return bytes;
595
+ }
596
+
597
+ function componentBreakdown(
598
+ composition: FusionBudgetStageComposition | undefined,
599
+ route: FusionRouteCapacity,
600
+ ): FusionBudgetComponentBreakdown {
601
+ const empty = {
602
+ visible_text_bytes: 0,
603
+ omission_receipt_bytes: 0,
604
+ projection_metadata_bytes: 0,
605
+ request_bytes: 0,
606
+ static_stage_framing_bytes: 0,
607
+ upstream_output_contract_bytes: 0,
608
+ } satisfies FusionBudgetStageComposition;
609
+ const item = composition ?? empty;
610
+ return {
611
+ visible_text: {
612
+ bytes: item.visible_text_bytes,
613
+ tokens: segmentTokensForBytes(route, item.visible_text_bytes),
614
+ },
615
+ omission_receipts: {
616
+ bytes: item.omission_receipt_bytes,
617
+ tokens: segmentTokensForBytes(route, item.omission_receipt_bytes),
618
+ },
619
+ projection_metadata: {
620
+ bytes: item.projection_metadata_bytes,
621
+ tokens: segmentTokensForBytes(route, item.projection_metadata_bytes),
622
+ },
623
+ request: {
624
+ bytes: item.request_bytes,
625
+ tokens: segmentTokensForBytes(route, item.request_bytes),
626
+ },
627
+ static_stage_framing: {
628
+ bytes: item.static_stage_framing_bytes,
629
+ tokens: segmentTokensForBytes(route, item.static_stage_framing_bytes),
630
+ },
631
+ upstream_output_contracts: {
632
+ bytes: item.upstream_output_contract_bytes,
633
+ tokens: contractTokens(item.upstream_output_contract_bytes),
634
+ },
635
+ };
636
+ }
637
+
638
+ function medianCounterfactual(primary: FusionBudgetBlocker): FusionBudgetCounterfactuals['at_median_rate'] {
639
+ if (!primary.route.rate_source.backed) {
640
+ return { forecast_input_tokens_upper_bound: null, signed_headroom_tokens: null, fits: null };
641
+ }
642
+ const median = primary.route.rate_source.provenance.median_bpt_x1000;
643
+ if (median === null) return { forecast_input_tokens_upper_bound: null, signed_headroom_tokens: null, fits: null };
644
+ const variableTokens = ceilDiv(primary.input_utf8_bytes * 1000, median);
645
+ const tokens = variableTokens + TOKEN_BUDGET_AFFINE_F_TOKENS;
646
+ return {
647
+ forecast_input_tokens_upper_bound: tokens,
648
+ signed_headroom_tokens: primary.allowed_input_tokens - tokens,
649
+ fits: tokens <= primary.allowed_input_tokens,
650
+ };
651
+ }
652
+
653
+ function inputCounterfactuals(
654
+ primary: FusionBudgetBlocker,
655
+ plan: FusionBudgetPlanV1,
656
+ ): FusionBudgetCounterfactuals {
657
+ return {
658
+ empty_request: plan.empty_request,
659
+ without_reservation: {
660
+ forecast_input_tokens_upper_bound: primary.input_only_input_tokens_upper_bound,
661
+ signed_headroom_tokens: primary.input_only_signed_headroom_tokens,
662
+ fits: primary.fits,
663
+ },
664
+ at_median_rate: medianCounterfactual(primary),
665
+ };
666
+ }
667
+
668
+ function budgetErrorCode(
669
+ checkKind: FusionBudgetCheckKind,
670
+ ): 'prompt_budget_exceeded_forecast' | 'prompt_budget_exceeded_measured' {
671
+ return checkKind === 'rendered_prompt'
672
+ ? 'prompt_budget_exceeded_measured'
673
+ : 'prompt_budget_exceeded_forecast';
674
+ }
675
+
387
676
  export class FusionBudget {
388
677
  readonly routes: readonly FusionRouteCapacity[];
389
678
  readonly limiting: FusionRouteCapacity;
@@ -399,6 +688,19 @@ export class FusionBudget {
399
688
  return this.limiting.allowed_input_tokens;
400
689
  }
401
690
 
691
+ get resultRateSources() {
692
+ return this.routes.map((route) => route.rate_source);
693
+ }
694
+
695
+ get unknownProviderWarnings(): readonly string[] {
696
+ return this.routes.flatMap((route) => {
697
+ const warning = route.rate_source.warning;
698
+ return route.rate_source.backed || warning === null
699
+ ? []
700
+ : [`${route.qualified_id}: ${warning}`];
701
+ });
702
+ }
703
+
402
704
  private routeForStage(stage: FusionBudgetStage, slot?: 1 | 2 | 3): FusionRouteCapacity {
403
705
  if (stage === 'candidate') return routeByRole(this.routes, candidateRole(slot ?? 1));
404
706
  if (stage === 'merge') return routeByRole(this.routes, 'merger');
@@ -517,8 +819,7 @@ export class FusionBudget {
517
819
  const emptyBlockers = selectBlockers(emptyEntries);
518
820
  let reduction = 0;
519
821
  for (const entry of entries) {
520
- const byteLimit = entry.allowed_input_tokens * FUSION_BYTES_PER_TOKEN_DIVISOR;
521
- reduction = Math.max(reduction, entry.forecast_utf8_bytes - byteLimit);
822
+ reduction = Math.max(reduction, entry.input_utf8_bytes - maxKnownTextBytes(entry.route));
522
823
  }
523
824
  reduction = Math.max(0, reduction);
524
825
  const requestBytes = utf8Bytes(input.request.text);
@@ -537,13 +838,18 @@ export class FusionBudget {
537
838
  plan: FusionBudgetPlanV1,
538
839
  artifactDir: string,
539
840
  measurementKind: FusionBudgetErrorDetail['measurement_kind'],
841
+ checkKind: FusionBudgetCheckKind,
540
842
  ): FusionError {
541
- const remediation = remediationFor(plan.empty_request);
843
+ const composition = plan.primary_blocker_composition;
844
+ const dominantByteClass = primary.input_only_estimate.rateSource.dominant_byte_class;
845
+ const remediation = dominantRemediation(plan.empty_request, composition, dominantByteClass);
846
+ const tokensOver = primary.input_only_input_tokens_upper_bound - primary.allowed_input_tokens;
542
847
  const budget: FusionBudgetErrorDetail = {
543
848
  budget_stage: primary.budget_stage,
544
849
  measurement_kind: measurementKind,
545
- measured_utf8_bytes: primary.forecast_utf8_bytes,
546
- measured_input_tokens_upper_bound: primary.forecast_input_tokens_upper_bound,
850
+ check_kind: checkKind,
851
+ measured_utf8_bytes: primary.input_utf8_bytes,
852
+ measured_input_tokens_upper_bound: primary.input_only_input_tokens_upper_bound,
547
853
  allowed_input_tokens: primary.allowed_input_tokens,
548
854
  limiting_model: {
549
855
  provider: primary.route.provider,
@@ -551,30 +857,57 @@ export class FusionBudget {
551
857
  qualified_id: primary.route.qualified_id,
552
858
  context_window_tokens: primary.route.context_window_tokens,
553
859
  },
860
+ rate_source: primary.input_only_estimate.rateSource,
861
+ backed: primary.input_only_estimate.rateSource.backed,
862
+ dominant_byte_class: dominantByteClass,
863
+ component_breakdown: componentBreakdown(composition, primary.route),
864
+ byte_class_breakdown: primary.input_only_estimate.byte_class_breakdown,
865
+ dense_regions: [],
866
+ bytes_over: primary.bytes_over,
867
+ tokens_over: Math.max(0, tokensOver),
868
+ required_allowed_tokens: primary.input_only_input_tokens_upper_bound,
869
+ route_table: routeTable(this.routes),
870
+ counterfactuals: inputCounterfactuals(primary, plan),
871
+ stage_upstream_actuals: [],
872
+ policy_id: FUSION_BUDGET_POLICY.id,
873
+ calibration_version: TOKEN_BUDGET_CALIBRATION_VERSION,
554
874
  context_policy_id: this.contextPolicyId,
555
875
  remediation,
556
876
  blockers: plan.blockers,
557
877
  artifact_dir: artifactDir,
558
878
  };
559
879
  if (primary.slot !== undefined) budget.slot = primary.slot;
560
- const composition = plan.primary_blocker_composition;
561
880
  const compositionText = composition === undefined ? 'unavailable' : formatComposition(composition);
562
881
  const additional = plan.blockers
563
882
  .filter((blocker) => blocker !== primary)
564
883
  .map((blocker) => `${entryLabel(blocker)} route=${blocker.route.qualified_id}`)
565
884
  .join('; ');
566
- const overage = primary.forecast_input_tokens_upper_bound - primary.allowed_input_tokens;
885
+ const rateText = `${primary.route.family} ${formatRateX100(primary.input_only_estimate.rateSource.effective_rate_bytes_per_token_x100)} B/tok + ${String(primary.input_only_estimate.rateSource.affine_f_tokens)} tokens (${primary.input_only_estimate.rateSource.source}, backed=${String(primary.input_only_estimate.rateSource.backed)}, dominant=${dominantByteClass})`;
886
+ const sourceWarning = primary.input_only_estimate.rateSource.warning;
887
+ const routeWarning = sourceWarning === null ? '' : ` Rate warning: ${sourceWarning}.`;
888
+ const checkText =
889
+ checkKind === 'input_only_preflight'
890
+ ? 'input-only preflight forecast'
891
+ : 'exact rendered prompt measurement';
892
+ const dominantText =
893
+ dominantByteClass === 'multibyte'
894
+ ? ' Dominant byte class is multibyte UTF-8; the fatal gate uses the conservative multibyte rate and the plan separately records the provable 1.00 B/tok advisory ceiling.'
895
+ : dominantByteClass === 'dense_ascii'
896
+ ? ' Dominant byte class is dense ASCII/low-whitespace content; the whitespace gate is a heuristic token-density proxy, not a bound.'
897
+ : ` Dominant byte class is ${dominantByteClass}.`;
567
898
  const message =
568
- `Fusion prompt budget exceeded before child creation. Primary blocking stage: ${entryLabel(primary)} on route ${primary.route.qualified_id}. ` +
569
- `Forecast ${String(primary.forecast_utf8_bytes)} UTF-8 bytes (<= ${String(primary.forecast_input_tokens_upper_bound)} input tokens) against ${String(primary.allowed_input_tokens)} allowed input tokens, over by ${String(overage)} tokens. ` +
899
+ `Fusion prompt budget exceeded by ${checkText} before child creation. Primary blocking stage: ${entryLabel(primary)} on route ${primary.route.qualified_id}. ` +
900
+ `Forecast ${String(primary.input_utf8_bytes)} UTF-8 bytes (<= ${String(primary.input_only_input_tokens_upper_bound)} input tokens) against ${String(primary.allowed_input_tokens)} allowed input tokens, over by ${String(Math.max(0, tokensOver))} tokens. ` +
901
+ `Estimator: ${rateText}.${routeWarning}${dominantText} ` +
570
902
  `No child was created. Nothing was clipped, dropped, or substituted. Artifact directory: ${artifactDir}.\n` +
571
903
  `Per-stage forecast table:\n${formatTable(plan.stages)}\n` +
572
904
  `Primary blocker byte composition: ${compositionText}.\n` +
573
905
  `Additional blockers: ${additional.length === 0 ? 'none' : additional}.\n` +
574
906
  `${formatEmptyRequestVerdict(plan.empty_request)}\n` +
907
+ `Route byte-capacity order: ${routeTable(this.routes).map((route) => `${route.qualified_id}=${String(route.byte_capacity_utf8_bytes)}B`).join(', ')}.\n` +
575
908
  `Remediation: ${remediation.join(' ')}`;
576
909
  const details = {
577
- code: 'prompt_budget_exceeded' as const,
910
+ code: budgetErrorCode(checkKind),
578
911
  childCreated: false,
579
912
  budget,
580
913
  stage: stageFromBudgetStage(primary.budget_stage),
@@ -607,7 +940,13 @@ export class FusionBudget {
607
940
 
608
941
  assertPlanFits(plan: FusionBudgetPlanV1, artifactDir: string): void {
609
942
  if (plan.primary_blocker !== undefined) {
610
- throw this.failure(plan.primary_blocker, plan, artifactDir, 'stage_forecast');
943
+ throw this.failure(
944
+ plan.primary_blocker,
945
+ plan,
946
+ artifactDir,
947
+ 'stage_forecast',
948
+ 'input_only_preflight',
949
+ );
611
950
  }
612
951
  }
613
952
 
@@ -618,19 +957,29 @@ export class FusionBudget {
618
957
  slot?: 1 | 2 | 3,
619
958
  ): void {
620
959
  const route = this.routeForStage(stage, slot);
621
- const forecastUtf8Bytes = utf8Bytes(systemPrompt) + utf8Bytes(userPrompt);
622
- const tokens = fusionTokenUpperBound(forecastUtf8Bytes);
623
- if (tokens <= route.allowed_input_tokens) return;
960
+ const inputSegments = [knownTextSegment(systemPrompt), knownTextSegment(userPrompt)];
961
+ const inputBytes = inputSegments.reduce((sum, segment) => sum + segment.bytes, 0);
962
+ const estimate = estimateRouteInput(route, inputSegments);
963
+ if (estimate.tokens <= route.allowed_input_tokens) return;
624
964
  const entry: FusionStageBudgetPlanEntry = {
625
965
  budget_stage: stage,
626
966
  route,
627
967
  conditional: stage === 'evaluation_repair',
628
- forecast_utf8_bytes: forecastUtf8Bytes,
629
- forecast_input_tokens_upper_bound: tokens,
968
+ check_kind: 'input_only_preflight',
969
+ input_utf8_bytes: inputBytes,
970
+ upstream_output_contract_bytes: 0,
971
+ forecast_utf8_bytes: inputBytes,
972
+ input_only_input_tokens_upper_bound: estimate.tokens,
973
+ forecast_input_tokens_upper_bound: estimate.tokens,
630
974
  allowed_input_tokens: route.allowed_input_tokens,
631
- signed_headroom_tokens: route.allowed_input_tokens - tokens,
632
- utilization: tokens / route.allowed_input_tokens,
975
+ input_only_signed_headroom_tokens: route.allowed_input_tokens - estimate.tokens,
976
+ signed_headroom_tokens: route.allowed_input_tokens - estimate.tokens,
977
+ input_only_utilization_basis_points: utilizationBasisPoints(estimate.tokens, route.allowed_input_tokens),
978
+ utilization_basis_points: utilizationBasisPoints(estimate.tokens, route.allowed_input_tokens),
979
+ input_only_estimate: estimate,
980
+ reservation_estimate: estimate,
633
981
  fits: false,
982
+ reservation_fits: false,
634
983
  };
635
984
  if (slot !== undefined) entry.slot = slot;
636
985
  const blocker = blockerFromEntry(entry);
@@ -645,12 +994,53 @@ export class FusionBudget {
645
994
  request_utf8_bytes: 0,
646
995
  still_fails_with_empty_request: true,
647
996
  shortening_request_can_help: false,
648
- minimum_request_byte_reduction: forecastUtf8Bytes - route.allowed_input_tokens * FUSION_BYTES_PER_TOKEN_DIVISOR,
997
+ minimum_request_byte_reduction: blocker.bytes_over,
649
998
  maximum_safe_request_utf8_bytes: 0,
650
999
  blockers_with_empty_request: [blocker],
651
1000
  },
652
1001
  warnings: [],
653
1002
  };
654
- throw this.failure(blocker, plan, 'stage prompt re-measurement', 'rendered_prompt');
1003
+ throw this.failure(blocker, plan, 'stage prompt re-measurement', 'rendered_prompt', 'rendered_prompt');
1004
+ }
1005
+
1006
+ calibrationViolationForCompletedChild(
1007
+ stage: FusionStage,
1008
+ systemPrompt: string,
1009
+ userPrompt: string,
1010
+ result: FusionChildRunResult,
1011
+ slot?: 1 | 2 | 3,
1012
+ ): FusionCalibrationViolation | undefined {
1013
+ const route = this.routeForStage(stage, slot);
1014
+ const inputSegments = [knownTextSegment(systemPrompt), knownTextSegment(userPrompt)];
1015
+ const promptUtf8Bytes = inputSegments.reduce((sum, segment) => sum + segment.bytes, 0);
1016
+ const estimate = estimateRouteInput(route, inputSegments);
1017
+ const billedInput = result.usage.input + result.usage.cacheRead + result.usage.cacheWrite;
1018
+ if (billedInput <= estimate.tokens) return undefined;
1019
+ const violation: FusionCalibrationViolation = {
1020
+ schema_version: FUSION_CALIBRATION_VIOLATION_SCHEMA_VERSION,
1021
+ stage,
1022
+ attempt: result.attempt,
1023
+ route: {
1024
+ provider: result.provider,
1025
+ model: result.model,
1026
+ qualified_id: result.qualifiedId,
1027
+ },
1028
+ family: route.family,
1029
+ rate_source: estimate.rateSource,
1030
+ prompt_utf8_bytes: promptUtf8Bytes,
1031
+ prompt_sha256: sha256Hex(`${systemPrompt}\u0000${userPrompt}`),
1032
+ forecast_input_tokens: estimate.tokens,
1033
+ billed_input_tokens: billedInput,
1034
+ billed_input_breakdown: {
1035
+ input: result.usage.input,
1036
+ cache_read: result.usage.cacheRead,
1037
+ cache_write: result.usage.cacheWrite,
1038
+ },
1039
+ under_forecast_tokens: billedInput - estimate.tokens,
1040
+ byte_class_breakdown: estimate.byte_class_breakdown,
1041
+ dominant_byte_class: estimate.rateSource.dominant_byte_class,
1042
+ };
1043
+ if (slot !== undefined) violation.slot = slot;
1044
+ return violation;
655
1045
  }
656
1046
  }