pi-background-tasks 0.7.4 → 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.
- package/PUBLISHING.md +7 -7
- package/README.md +199 -15
- package/TESTING.md +95 -1
- package/TEST_PLAN.md +21 -4
- package/extensions/delegate-child.ts +1 -0
- package/package.json +4 -3
- package/src/core/common.ts +41 -0
- package/src/core/context/parent-snapshot.ts +142 -0
- package/src/core/context/token-budget.ts +890 -0
- package/src/core/context/visible-conversation-v2.ts +551 -0
- package/src/core/delegate/artifacts.ts +479 -0
- package/src/core/delegate/budget.ts +370 -0
- package/src/core/delegate/hook-contract-evidence.json +18 -0
- package/src/core/delegate/hook-contract.ts +153 -0
- package/src/core/delegate/launch.ts +459 -0
- package/src/core/delegate/result-package.ts +443 -0
- package/src/core/delegate/runner.ts +406 -0
- package/src/core/delegate/seed.ts +411 -0
- package/src/core/delegate/types.ts +304 -0
- package/src/core/fusion/artifacts.ts +17 -2
- package/src/core/fusion/budget.ts +884 -210
- package/src/core/fusion/context.ts +118 -520
- package/src/core/fusion/orchestrator.ts +133 -20
- package/src/core/fusion/prompts.ts +11 -7
- package/src/core/fusion/types.ts +230 -86
- package/src/core/registry.ts +174 -0
- package/src/delegate-child-extension.ts +673 -0
- package/src/delegate-extension.ts +587 -0
- package/src/extension.ts +10 -0
- package/src/fusion-extension.ts +4 -0
|
@@ -1,170 +1,213 @@
|
|
|
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';
|
|
16
|
+
import {
|
|
17
|
+
FUSION_CANDIDATE_SYSTEM_PROMPT,
|
|
18
|
+
FUSION_EVALUATION_REPAIR_SYSTEM_PROMPT,
|
|
19
|
+
FUSION_EVALUATOR_SYSTEM_PROMPT,
|
|
20
|
+
FUSION_MERGER_SYSTEM_PROMPT,
|
|
21
|
+
buildBlindEvaluationInput,
|
|
22
|
+
buildCandidatePrompt,
|
|
23
|
+
buildEvaluationPrompt,
|
|
24
|
+
buildEvaluationRepairPrompt,
|
|
25
|
+
buildMergeInput,
|
|
26
|
+
buildMergePrompt,
|
|
27
|
+
type AnonymousFusionCandidate,
|
|
28
|
+
} from './prompts.js';
|
|
1
29
|
import {
|
|
2
30
|
FUSION_BUDGET_PLAN_SCHEMA_VERSION,
|
|
31
|
+
FUSION_CALIBRATION_VIOLATION_SCHEMA_VERSION,
|
|
32
|
+
FUSION_EVALUATION_SCHEMA_VERSION,
|
|
3
33
|
FusionError,
|
|
34
|
+
type FusionBudgetBlocker,
|
|
35
|
+
type FusionBudgetCheckKind,
|
|
36
|
+
type FusionBudgetComponentBreakdown,
|
|
37
|
+
type FusionBudgetCounterfactuals,
|
|
38
|
+
type FusionBudgetEmptyRequestVerdict,
|
|
4
39
|
type FusionBudgetErrorDetail,
|
|
5
40
|
type FusionBudgetPlanV1,
|
|
6
41
|
type FusionBudgetPolicyDescriptor,
|
|
42
|
+
type FusionBudgetRouteTableEntry,
|
|
7
43
|
type FusionBudgetStage,
|
|
44
|
+
type FusionBudgetStageComposition,
|
|
45
|
+
type FusionBudgetWarning,
|
|
46
|
+
type FusionCalibrationViolation,
|
|
47
|
+
type FusionCanonicalInputV3,
|
|
48
|
+
type FusionEvaluationV1,
|
|
8
49
|
type FusionRouteCapacity,
|
|
9
50
|
type FusionStage,
|
|
10
51
|
type FusionStageBudgetPlanEntry,
|
|
52
|
+
type FusionChildRunResult,
|
|
11
53
|
type ResolvedFusionModel,
|
|
12
54
|
type ResolvedFusionModels,
|
|
13
55
|
} from './types.js';
|
|
14
56
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
*
|
|
18
|
-
* Token upper bound = ceil(utf8Bytes / FUSION_BYTES_PER_TOKEN_DIVISOR).
|
|
19
|
-
*
|
|
20
|
-
* Across 159 real large Fusion prompts (50 KB–1.4 MB) recorded in
|
|
21
|
-
* `.pi/fusion/**\/manifest.json`, the smallest observed ratio was 3.552 bytes
|
|
22
|
-
* per reported input token. A divisor of 2 therefore leaves roughly a 1.7x
|
|
23
|
-
* margin against the densest real prompt and still bounds pathological input
|
|
24
|
-
* such as dense CJK (3 UTF-8 bytes producing at most ~1.5 tokens) or long runs
|
|
25
|
-
* of punctuation. It is not an estimate of typical usage; it is a ceiling.
|
|
26
|
-
*/
|
|
27
|
-
export const FUSION_BYTES_PER_TOKEN_DIVISOR = 2;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Enforced maximum size of one child's response, measured in **JSON-rendered
|
|
31
|
-
* transfer bytes** — the bytes the response actually contributes when a later
|
|
32
|
-
* stage embeds it, escaping included.
|
|
33
|
-
*
|
|
34
|
-
* Measuring the rendered form rather than the raw form removes the need to
|
|
35
|
-
* guess an escaping factor: quotes, backslashes, and newlines expand 2x and
|
|
36
|
-
* control characters up to 6x, so a raw-byte contract would not bound the
|
|
37
|
-
* embedded size. `assertChildOutputWithinContract` rejects a response that
|
|
38
|
-
* exceeds its stage bound, so a later stage can never be handed more embedded
|
|
39
|
-
* text than is budgeted here. Nothing is truncated to fit: an oversized
|
|
40
|
-
* response is a loud failure.
|
|
41
|
-
*
|
|
42
|
-
* Sized above the largest real responses recorded across `.pi/fusion`:
|
|
43
|
-
* candidate 45,434 B, evaluator 54,829 B, merged 56,846 B.
|
|
44
|
-
*/
|
|
57
|
+
export const FUSION_CALIBRATED_BYTES_PER_TOKEN = TOKEN_BUDGET_FAMILY_CALIBRATIONS;
|
|
58
|
+
|
|
45
59
|
export const FUSION_CANDIDATE_MAX_OUTPUT_BYTES = 48 * 1024;
|
|
46
60
|
export const FUSION_EVALUATION_MAX_OUTPUT_BYTES = 64 * 1024;
|
|
47
61
|
export const FUSION_MERGE_MAX_OUTPUT_BYTES = 64 * 1024;
|
|
48
|
-
|
|
49
|
-
/** `boundedEvaluationErrors` caps repair diagnostics far below this. */
|
|
50
62
|
export const FUSION_DIAGNOSTICS_MAX_BYTES = 8 * 1024;
|
|
51
63
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
* Derived from the largest output contract through the same conservative
|
|
56
|
-
* byte-to-token conversion used to measure prompts, so it cannot be understated.
|
|
57
|
-
*/
|
|
58
|
-
export const FUSION_RESERVED_OUTPUT_TOKENS = Math.ceil(
|
|
59
|
-
FUSION_MERGE_MAX_OUTPUT_BYTES / FUSION_BYTES_PER_TOKEN_DIVISOR,
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Upper bound on Pi/provider framing that is not represented in the prompt
|
|
64
|
-
* bytes Fusion renders: the child system prompt is passed via argv (counted
|
|
65
|
-
* separately below), but chat framing, tool-free scaffolding, and provider
|
|
66
|
-
* envelope overhead are not visible to this package.
|
|
67
|
-
*/
|
|
68
|
-
export const FUSION_FRAMING_RESERVE_TOKENS = 4_096;
|
|
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;
|
|
69
67
|
|
|
70
|
-
|
|
71
|
-
|
|
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
|
+
}
|
|
72
78
|
|
|
73
|
-
|
|
74
|
-
*
|
|
75
|
-
|
|
76
|
-
* object's own keys. These are constant-size, not content-dependent; 16 KiB is
|
|
77
|
-
* an order of magnitude above the real wrapper size.
|
|
78
|
-
*/
|
|
79
|
-
export const FUSION_WRAPPER_OVERHEAD_BYTES = 16 * 1024;
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Bytes of downstream growth the canonical input must leave room for.
|
|
83
|
-
*
|
|
84
|
-
* The widest stage prompt is the evaluation repair, which embeds the canonical
|
|
85
|
-
* input plus three candidate answers, the invalid evaluator output, and bounded
|
|
86
|
-
* validation errors. The merge prompt is strictly smaller.
|
|
87
|
-
*
|
|
88
|
-
* Because the output contracts above are enforced against JSON-rendered bytes,
|
|
89
|
-
* this is an exact sum rather than a raw size inflated by an estimated escaping
|
|
90
|
-
* factor. No content can expand past it, and the exact rendered prompt is still
|
|
91
|
-
* re-measured before every spawn as defence in depth.
|
|
92
|
-
*/
|
|
93
|
-
export const FUSION_DOWNSTREAM_RESERVE_BYTES =
|
|
94
|
-
3 * FUSION_CANDIDATE_MAX_OUTPUT_BYTES +
|
|
95
|
-
FUSION_EVALUATION_MAX_OUTPUT_BYTES +
|
|
96
|
-
FUSION_DIAGNOSTICS_MAX_BYTES +
|
|
97
|
-
FUSION_WRAPPER_OVERHEAD_BYTES;
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Tokens withheld from the canonical input for that growth.
|
|
101
|
-
*
|
|
102
|
-
* This converts the reserve through the same byte-to-token function used to
|
|
103
|
-
* measure prompts. Reserving output *tokens* directly would understate the cost
|
|
104
|
-
* of re-embedding those bytes by the ratio between a provider's real
|
|
105
|
-
* tokenization and this conservative ceiling.
|
|
106
|
-
*/
|
|
107
|
-
export const FUSION_DOWNSTREAM_RESERVE_TOKENS = Math.ceil(
|
|
108
|
-
FUSION_DOWNSTREAM_RESERVE_BYTES / FUSION_BYTES_PER_TOKEN_DIVISOR,
|
|
79
|
+
export const FUSION_RESERVED_OUTPUT_TOKENS = ceilDiv(
|
|
80
|
+
FUSION_MERGE_MAX_OUTPUT_BYTES * TOKEN_BUDGET_RATE_SCALE,
|
|
81
|
+
FUSION_OUTPUT_RESERVE_RATE_X100,
|
|
109
82
|
);
|
|
110
|
-
|
|
111
|
-
|
|
83
|
+
export const FUSION_FRAMING_RESERVE_TOKENS = 0;
|
|
84
|
+
export const FUSION_SAFETY_RESERVE_TOKENS = 4_096;
|
|
112
85
|
export const FUSION_MIN_CANONICAL_INPUT_TOKENS = 8_192;
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Smallest context window that can host the complete workflow under this policy.
|
|
116
|
-
*
|
|
117
|
-
* This is a consequence of uniformly conservative accounting, not a preference
|
|
118
|
-
* for large models: the downstream reserve, the output reserve, framing, safety,
|
|
119
|
-
* and a usable amount of canonical input must all fit. Routes below this are
|
|
120
|
-
* rejected at configuration time with an actionable error rather than being
|
|
121
|
-
* silently accepted and failing later at the provider.
|
|
122
|
-
*/
|
|
123
86
|
export const FUSION_MIN_CONTEXT_WINDOW_TOKENS =
|
|
124
|
-
FUSION_DOWNSTREAM_RESERVE_TOKENS +
|
|
125
87
|
FUSION_MIN_CANONICAL_INPUT_TOKENS +
|
|
126
88
|
FUSION_RESERVED_OUTPUT_TOKENS +
|
|
127
89
|
FUSION_FRAMING_RESERVE_TOKENS +
|
|
128
90
|
FUSION_SAFETY_RESERVE_TOKENS;
|
|
129
91
|
|
|
130
92
|
export const FUSION_BUDGET_POLICY: FusionBudgetPolicyDescriptor = {
|
|
131
|
-
id: 'fusion-budget-policy-
|
|
132
|
-
|
|
93
|
+
id: 'fusion-budget-policy-v3',
|
|
94
|
+
calibration_version: TOKEN_BUDGET_CALIBRATION_VERSION,
|
|
95
|
+
calibration_table: FUSION_CALIBRATED_BYTES_PER_TOKEN,
|
|
133
96
|
reserved_output_tokens: FUSION_RESERVED_OUTPUT_TOKENS,
|
|
134
97
|
framing_reserve_tokens: FUSION_FRAMING_RESERVE_TOKENS,
|
|
135
98
|
safety_reserve_tokens: FUSION_SAFETY_RESERVE_TOKENS,
|
|
136
|
-
|
|
137
|
-
|
|
99
|
+
candidate_output_contract_bytes: FUSION_CANDIDATE_MAX_OUTPUT_BYTES,
|
|
100
|
+
evaluation_output_contract_bytes: FUSION_EVALUATION_MAX_OUTPUT_BYTES,
|
|
101
|
+
merge_output_contract_bytes: FUSION_MERGE_MAX_OUTPUT_BYTES,
|
|
102
|
+
diagnostics_contract_bytes: FUSION_DIAGNOSTICS_MAX_BYTES,
|
|
103
|
+
utilization_warning_threshold_basis_points: FUSION_UTILIZATION_WARNING_THRESHOLD_BASIS_POINTS,
|
|
138
104
|
};
|
|
139
105
|
|
|
140
|
-
const
|
|
106
|
+
const EMPTY_REMEDIATION: readonly string[] = Object.freeze([
|
|
141
107
|
'Start a fresh Pi conversation, or run Fusion earlier in the session.',
|
|
142
|
-
'
|
|
108
|
+
"Raise the route's context window with a larger-context model via /fusion-models.",
|
|
143
109
|
'Restate only the required prior findings as visible conversation text.',
|
|
144
|
-
'Configure a larger-context model for the limiting Fusion slot via /fusion-models.',
|
|
145
110
|
]);
|
|
146
111
|
|
|
112
|
+
const REQUEST_REMEDIATION: readonly string[] = Object.freeze([
|
|
113
|
+
'Provide a shorter, self-contained fusion_brainstorm prompt.',
|
|
114
|
+
'Start a fresh Pi conversation, or run Fusion earlier in the session.',
|
|
115
|
+
"Raise the route's context window with a larger-context model via /fusion-models.",
|
|
116
|
+
]);
|
|
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
|
+
|
|
135
|
+
const EMPTY_CANDIDATES: readonly [
|
|
136
|
+
AnonymousFusionCandidate,
|
|
137
|
+
AnonymousFusionCandidate,
|
|
138
|
+
AnonymousFusionCandidate,
|
|
139
|
+
] = Object.freeze([
|
|
140
|
+
Object.freeze({ candidate_id: 'A', response: '' }),
|
|
141
|
+
Object.freeze({ candidate_id: 'B', response: '' }),
|
|
142
|
+
Object.freeze({ candidate_id: 'C', response: '' }),
|
|
143
|
+
]);
|
|
144
|
+
|
|
145
|
+
const EMPTY_EVALUATION: FusionEvaluationV1 = Object.freeze({
|
|
146
|
+
schema_version: FUSION_EVALUATION_SCHEMA_VERSION,
|
|
147
|
+
candidate_assessments: Object.freeze([
|
|
148
|
+
Object.freeze({
|
|
149
|
+
candidate_id: 'A',
|
|
150
|
+
summary: '',
|
|
151
|
+
strengths: Object.freeze([]),
|
|
152
|
+
limitations: Object.freeze([]),
|
|
153
|
+
useful_contributions: Object.freeze([]),
|
|
154
|
+
risks: Object.freeze([]),
|
|
155
|
+
}),
|
|
156
|
+
Object.freeze({
|
|
157
|
+
candidate_id: 'B',
|
|
158
|
+
summary: '',
|
|
159
|
+
strengths: Object.freeze([]),
|
|
160
|
+
limitations: Object.freeze([]),
|
|
161
|
+
useful_contributions: Object.freeze([]),
|
|
162
|
+
risks: Object.freeze([]),
|
|
163
|
+
}),
|
|
164
|
+
Object.freeze({
|
|
165
|
+
candidate_id: 'C',
|
|
166
|
+
summary: '',
|
|
167
|
+
strengths: Object.freeze([]),
|
|
168
|
+
limitations: Object.freeze([]),
|
|
169
|
+
useful_contributions: Object.freeze([]),
|
|
170
|
+
risks: Object.freeze([]),
|
|
171
|
+
}),
|
|
172
|
+
]) as readonly [
|
|
173
|
+
FusionEvaluationV1['candidate_assessments'][0],
|
|
174
|
+
FusionEvaluationV1['candidate_assessments'][1],
|
|
175
|
+
FusionEvaluationV1['candidate_assessments'][2],
|
|
176
|
+
],
|
|
177
|
+
agreements: Object.freeze([]),
|
|
178
|
+
conflicts: Object.freeze([]),
|
|
179
|
+
synthesis_plan: Object.freeze({
|
|
180
|
+
must_include: Object.freeze([]),
|
|
181
|
+
must_resolve: Object.freeze([]),
|
|
182
|
+
must_avoid: Object.freeze([]),
|
|
183
|
+
}),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
interface StageForecastDraft {
|
|
187
|
+
budget_stage: FusionBudgetStage;
|
|
188
|
+
slot?: 1 | 2 | 3;
|
|
189
|
+
route: FusionRouteCapacity;
|
|
190
|
+
conditional: boolean;
|
|
191
|
+
system_prompt: string;
|
|
192
|
+
empty_user_prompt: string;
|
|
193
|
+
upstream_output_contract_bytes: number;
|
|
194
|
+
}
|
|
195
|
+
|
|
147
196
|
export function fusionTokenUpperBound(utf8Bytes: number): number {
|
|
148
|
-
return
|
|
197
|
+
return estimateInputTokens({
|
|
198
|
+
family: 'unknown',
|
|
199
|
+
scope: 'conservative',
|
|
200
|
+
segments: [{ kind: 'known_text', bytes: utf8Bytes, multibyteBytes: 0, denseBytes: 0 }],
|
|
201
|
+
}).tokens;
|
|
149
202
|
}
|
|
150
203
|
|
|
151
|
-
/** Enforced response-size contract for one stage, in UTF-8 bytes. */
|
|
152
204
|
export function fusionOutputContractBytes(stage: FusionStage): number {
|
|
153
205
|
if (stage === 'candidate') return FUSION_CANDIDATE_MAX_OUTPUT_BYTES;
|
|
154
206
|
if (stage === 'evaluation') return FUSION_EVALUATION_MAX_OUTPUT_BYTES;
|
|
155
207
|
return FUSION_MERGE_MAX_OUTPUT_BYTES;
|
|
156
208
|
}
|
|
157
209
|
|
|
158
|
-
/**
|
|
159
|
-
* Reject a child response larger than its stage contract.
|
|
160
|
-
*
|
|
161
|
-
* This is what makes the downstream reserve a guarantee rather than a hope: a
|
|
162
|
-
* later stage can never embed more bytes than were budgeted. The oversized text
|
|
163
|
-
* is never sliced and never forwarded; the run fails loudly instead.
|
|
164
|
-
*/
|
|
165
210
|
export function assertChildOutputWithinContract(stage: FusionStage, text: string): void {
|
|
166
|
-
// Measure what the response costs once embedded, escaping included, so the
|
|
167
|
-
// downstream reserve is an exact bound rather than an estimate.
|
|
168
211
|
const bytes = Buffer.byteLength(JSON.stringify(text), 'utf8');
|
|
169
212
|
const allowed = fusionOutputContractBytes(stage);
|
|
170
213
|
if (bytes <= allowed) return;
|
|
@@ -174,9 +217,17 @@ export function assertChildOutputWithinContract(stage: FusionStage, text: string
|
|
|
174
217
|
);
|
|
175
218
|
}
|
|
176
219
|
|
|
220
|
+
function utf8Bytes(value: string): number {
|
|
221
|
+
return Buffer.byteLength(value, 'utf8');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function sha256Hex(value: string): string {
|
|
225
|
+
return createHash('sha256').update(value, 'utf8').digest('hex');
|
|
226
|
+
}
|
|
227
|
+
|
|
177
228
|
function requirePositiveContextWindow(model: ResolvedFusionModel, role: string): number {
|
|
178
229
|
const value = model.contextWindow;
|
|
179
|
-
if (!
|
|
230
|
+
if (!isUsableContextWindow(value)) {
|
|
180
231
|
throw new FusionError(
|
|
181
232
|
`fusion ${role} route ${model.qualifiedId} has no usable context window capacity`,
|
|
182
233
|
{ code: 'model_capacity_unknown', childCreated: false },
|
|
@@ -185,24 +236,45 @@ function requirePositiveContextWindow(model: ResolvedFusionModel, role: string):
|
|
|
185
236
|
return value;
|
|
186
237
|
}
|
|
187
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
|
+
|
|
188
245
|
function routeCapacity(
|
|
189
246
|
model: ResolvedFusionModel,
|
|
190
247
|
role: FusionRouteCapacity['role'],
|
|
191
248
|
): FusionRouteCapacity {
|
|
192
249
|
const contextWindow = requirePositiveContextWindow(model, role);
|
|
193
|
-
const allowed =
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
// input, otherwise the configured panel can never complete a workflow.
|
|
200
|
-
if (allowed < FUSION_DOWNSTREAM_RESERVE_TOKENS + FUSION_MIN_CANONICAL_INPUT_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
|
+
});
|
|
255
|
+
if (allowed < FUSION_MIN_CANONICAL_INPUT_TOKENS) {
|
|
201
256
|
throw new FusionError(
|
|
202
|
-
`fusion ${role} route ${model.qualifiedId} has a ${String(contextWindow)}-token context window, but
|
|
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.`,
|
|
203
258
|
{ code: 'model_capacity_unknown', childCreated: false },
|
|
204
259
|
);
|
|
205
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;
|
|
206
278
|
return {
|
|
207
279
|
role,
|
|
208
280
|
provider: model.provider,
|
|
@@ -213,6 +285,11 @@ function routeCapacity(
|
|
|
213
285
|
framing_reserve_tokens: FUSION_FRAMING_RESERVE_TOKENS,
|
|
214
286
|
safety_reserve_tokens: FUSION_SAFETY_RESERVE_TOKENS,
|
|
215
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
|
+
),
|
|
216
293
|
};
|
|
217
294
|
}
|
|
218
295
|
|
|
@@ -226,17 +303,17 @@ export function fusionRouteCapacities(models: ResolvedFusionModels): readonly Fu
|
|
|
226
303
|
];
|
|
227
304
|
}
|
|
228
305
|
|
|
229
|
-
/**
|
|
230
|
-
* The limiting route is the configured participant with the smallest input
|
|
231
|
-
* budget — never the largest-context model. Ties resolve to the first route in
|
|
232
|
-
* declaration order so the plan is deterministic.
|
|
233
|
-
*/
|
|
234
306
|
export function fusionLimitingRoute(
|
|
235
307
|
routes: readonly FusionRouteCapacity[],
|
|
236
308
|
): FusionRouteCapacity {
|
|
237
309
|
let limiting: FusionRouteCapacity | undefined;
|
|
238
310
|
for (const route of routes) {
|
|
239
|
-
if (
|
|
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
|
+
) {
|
|
240
317
|
limiting = route;
|
|
241
318
|
}
|
|
242
319
|
}
|
|
@@ -249,6 +326,353 @@ export function fusionLimitingRoute(
|
|
|
249
326
|
return limiting;
|
|
250
327
|
}
|
|
251
328
|
|
|
329
|
+
function routeByRole(
|
|
330
|
+
routes: readonly FusionRouteCapacity[],
|
|
331
|
+
role: FusionRouteCapacity['role'],
|
|
332
|
+
): FusionRouteCapacity {
|
|
333
|
+
const route = routes.find((item) => item.role === role);
|
|
334
|
+
if (route === undefined) {
|
|
335
|
+
throw new FusionError(`fusion budget route ${role} is missing`, {
|
|
336
|
+
code: 'model_capacity_unknown',
|
|
337
|
+
childCreated: false,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
return route;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function candidateRole(slot: 1 | 2 | 3): FusionRouteCapacity['role'] {
|
|
344
|
+
if (slot === 1) return 'candidate-1';
|
|
345
|
+
if (slot === 2) return 'candidate-2';
|
|
346
|
+
return 'candidate-3';
|
|
347
|
+
}
|
|
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
|
+
|
|
367
|
+
function forecastEntry(draft: StageForecastDraft): FusionStageBudgetPlanEntry {
|
|
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;
|
|
377
|
+
const entry: FusionStageBudgetPlanEntry = {
|
|
378
|
+
budget_stage: draft.budget_stage,
|
|
379
|
+
route: draft.route,
|
|
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,
|
|
384
|
+
forecast_utf8_bytes: forecastUtf8Bytes,
|
|
385
|
+
input_only_input_tokens_upper_bound: inputOnly.tokens,
|
|
386
|
+
forecast_input_tokens_upper_bound: reservation.tokens,
|
|
387
|
+
allowed_input_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,
|
|
402
|
+
};
|
|
403
|
+
if (draft.slot !== undefined) entry.slot = draft.slot;
|
|
404
|
+
return entry;
|
|
405
|
+
}
|
|
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
|
+
|
|
417
|
+
function blockerFromEntry(entry: FusionStageBudgetPlanEntry): FusionBudgetBlocker {
|
|
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
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function blockerOrder(blocker: FusionBudgetBlocker): number {
|
|
426
|
+
if (blocker.budget_stage === 'candidate') return blocker.slot ?? 1;
|
|
427
|
+
if (blocker.budget_stage === 'evaluation') return 4;
|
|
428
|
+
if (blocker.budget_stage === 'merge') return 5;
|
|
429
|
+
return 6;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function selectBlockers(entries: readonly FusionStageBudgetPlanEntry[]): readonly FusionBudgetBlocker[] {
|
|
433
|
+
return entries.filter((entry) => !entry.fits).map(blockerFromEntry).sort((left, right) => {
|
|
434
|
+
const byOrder = blockerOrder(left) - blockerOrder(right);
|
|
435
|
+
if (byOrder !== 0) return byOrder;
|
|
436
|
+
return left.route.role.localeCompare(right.route.role);
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function selectPrimaryBlocker(
|
|
441
|
+
blockers: readonly FusionBudgetBlocker[],
|
|
442
|
+
): FusionBudgetBlocker | undefined {
|
|
443
|
+
const mandatory = blockers.find((blocker) => !blocker.conditional);
|
|
444
|
+
return mandatory ?? blockers[0];
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function replaceRequestText(input: FusionCanonicalInputV3, text: string): FusionCanonicalInputV3 {
|
|
448
|
+
return {
|
|
449
|
+
...input,
|
|
450
|
+
request: {
|
|
451
|
+
...input.request,
|
|
452
|
+
text,
|
|
453
|
+
sha256: sha256Hex(text),
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function visibleTextBytes(input: FusionCanonicalInputV3): number {
|
|
459
|
+
let total = 0;
|
|
460
|
+
for (const entry of input.conversation_projection.entries) {
|
|
461
|
+
if (entry[0] === 't') total += utf8Bytes(JSON.stringify(entry[4]));
|
|
462
|
+
}
|
|
463
|
+
return total;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function omissionReceiptBytes(input: FusionCanonicalInputV3): number {
|
|
467
|
+
let total = 0;
|
|
468
|
+
for (const entry of input.conversation_projection.entries) {
|
|
469
|
+
if (entry[0] === 'o') total += utf8Bytes(JSON.stringify(entry));
|
|
470
|
+
}
|
|
471
|
+
return total;
|
|
472
|
+
}
|
|
473
|
+
|
|
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;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function warningsFor(entries: readonly FusionStageBudgetPlanEntry[]): readonly FusionBudgetWarning[] {
|
|
494
|
+
return entries.flatMap((entry) => {
|
|
495
|
+
const warning = warningFromEntry(entry);
|
|
496
|
+
return warning === undefined ? [] : [warning];
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function entryLabel(entry: FusionStageBudgetPlanEntry): string {
|
|
501
|
+
const slot = entry.slot === undefined ? '' : `-${String(entry.slot)}`;
|
|
502
|
+
const conditional = entry.conditional ? ' (conditional)' : '';
|
|
503
|
+
return `${entry.budget_stage}${slot}${conditional}`;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function formatTable(entries: readonly FusionStageBudgetPlanEntry[]): string {
|
|
507
|
+
const lines = entries.map(
|
|
508
|
+
(entry) =>
|
|
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'}`,
|
|
510
|
+
);
|
|
511
|
+
return lines.join('\n');
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function formatComposition(composition: FusionBudgetStageComposition): string {
|
|
515
|
+
return [
|
|
516
|
+
`visible text ${String(composition.visible_text_bytes)} B`,
|
|
517
|
+
`omission receipts ${String(composition.omission_receipt_bytes)} B`,
|
|
518
|
+
`projection metadata ${String(composition.projection_metadata_bytes)} B`,
|
|
519
|
+
`request ${String(composition.request_bytes)} B`,
|
|
520
|
+
`static stage framing ${String(composition.static_stage_framing_bytes)} B`,
|
|
521
|
+
`upstream output contracts ${String(composition.upstream_output_contract_bytes)} B`,
|
|
522
|
+
].join('; ');
|
|
523
|
+
}
|
|
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
|
+
|
|
546
|
+
function remediationFor(verdict: FusionBudgetEmptyRequestVerdict): readonly string[] {
|
|
547
|
+
return verdict.still_fails_with_empty_request ? EMPTY_REMEDIATION : REQUEST_REMEDIATION;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function formatEmptyRequestVerdict(verdict: FusionBudgetEmptyRequestVerdict): string {
|
|
551
|
+
if (verdict.still_fails_with_empty_request) {
|
|
552
|
+
return `Empty-request counterfactual: still fails with ${String(verdict.blockers_with_empty_request.length)} blocking stage(s), so shortening the request cannot make this workflow fit.`;
|
|
553
|
+
}
|
|
554
|
+
if (verdict.minimum_request_byte_reduction === 0) {
|
|
555
|
+
return 'Empty-request counterfactual: fits; no request reduction is required by the current plan.';
|
|
556
|
+
}
|
|
557
|
+
return `Empty-request counterfactual: fits, so request size determines feasibility; reduce the request by at least ${String(verdict.minimum_request_byte_reduction)} UTF-8 bytes, leaving at most ${String(verdict.maximum_safe_request_utf8_bytes)} UTF-8 bytes.`;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function stageFromBudgetStage(stage: FusionBudgetStage): FusionStage {
|
|
561
|
+
if (stage === 'merge') return 'merge';
|
|
562
|
+
if (stage === 'candidate') return 'candidate';
|
|
563
|
+
return 'evaluation';
|
|
564
|
+
}
|
|
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
|
+
|
|
252
676
|
export class FusionBudget {
|
|
253
677
|
readonly routes: readonly FusionRouteCapacity[];
|
|
254
678
|
readonly limiting: FusionRouteCapacity;
|
|
@@ -260,113 +684,363 @@ export class FusionBudget {
|
|
|
260
684
|
this.contextPolicyId = contextPolicyId;
|
|
261
685
|
}
|
|
262
686
|
|
|
263
|
-
/** Full input budget of the limiting route, in tokens. */
|
|
264
687
|
get allowedInputTokens(): number {
|
|
265
688
|
return this.limiting.allowed_input_tokens;
|
|
266
689
|
}
|
|
267
690
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
get
|
|
273
|
-
return this.
|
|
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
|
+
|
|
704
|
+
private routeForStage(stage: FusionBudgetStage, slot?: 1 | 2 | 3): FusionRouteCapacity {
|
|
705
|
+
if (stage === 'candidate') return routeByRole(this.routes, candidateRole(slot ?? 1));
|
|
706
|
+
if (stage === 'merge') return routeByRole(this.routes, 'merger');
|
|
707
|
+
return routeByRole(this.routes, 'evaluator');
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
private drafts(input: FusionCanonicalInputV3): readonly StageForecastDraft[] {
|
|
711
|
+
const candidatePrompt = buildCandidatePrompt(input);
|
|
712
|
+
const blindInput = buildBlindEvaluationInput(input, EMPTY_CANDIDATES);
|
|
713
|
+
const evaluationPrompt = buildEvaluationPrompt(blindInput);
|
|
714
|
+
const repairPrompt = buildEvaluationRepairPrompt({
|
|
715
|
+
schema_version: 'pi-background-tasks.fusion-evaluation-repair-input.v1',
|
|
716
|
+
original_blind_input: blindInput,
|
|
717
|
+
invalid_output: '',
|
|
718
|
+
validation_errors: [],
|
|
719
|
+
});
|
|
720
|
+
const mergePrompt = buildMergePrompt(buildMergeInput(input, EMPTY_CANDIDATES, EMPTY_EVALUATION));
|
|
721
|
+
return [
|
|
722
|
+
{
|
|
723
|
+
budget_stage: 'candidate',
|
|
724
|
+
slot: 1,
|
|
725
|
+
route: this.routeForStage('candidate', 1),
|
|
726
|
+
conditional: false,
|
|
727
|
+
system_prompt: FUSION_CANDIDATE_SYSTEM_PROMPT,
|
|
728
|
+
empty_user_prompt: candidatePrompt,
|
|
729
|
+
upstream_output_contract_bytes: 0,
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
budget_stage: 'candidate',
|
|
733
|
+
slot: 2,
|
|
734
|
+
route: this.routeForStage('candidate', 2),
|
|
735
|
+
conditional: false,
|
|
736
|
+
system_prompt: FUSION_CANDIDATE_SYSTEM_PROMPT,
|
|
737
|
+
empty_user_prompt: candidatePrompt,
|
|
738
|
+
upstream_output_contract_bytes: 0,
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
budget_stage: 'candidate',
|
|
742
|
+
slot: 3,
|
|
743
|
+
route: this.routeForStage('candidate', 3),
|
|
744
|
+
conditional: false,
|
|
745
|
+
system_prompt: FUSION_CANDIDATE_SYSTEM_PROMPT,
|
|
746
|
+
empty_user_prompt: candidatePrompt,
|
|
747
|
+
upstream_output_contract_bytes: 0,
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
budget_stage: 'evaluation',
|
|
751
|
+
route: this.routeForStage('evaluation'),
|
|
752
|
+
conditional: false,
|
|
753
|
+
system_prompt: FUSION_EVALUATOR_SYSTEM_PROMPT,
|
|
754
|
+
empty_user_prompt: evaluationPrompt,
|
|
755
|
+
upstream_output_contract_bytes: 3 * FUSION_CANDIDATE_MAX_OUTPUT_BYTES,
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
budget_stage: 'merge',
|
|
759
|
+
route: this.routeForStage('merge'),
|
|
760
|
+
conditional: false,
|
|
761
|
+
system_prompt: FUSION_MERGER_SYSTEM_PROMPT,
|
|
762
|
+
empty_user_prompt: mergePrompt,
|
|
763
|
+
upstream_output_contract_bytes:
|
|
764
|
+
3 * FUSION_CANDIDATE_MAX_OUTPUT_BYTES + FUSION_EVALUATION_MAX_OUTPUT_BYTES,
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
budget_stage: 'evaluation_repair',
|
|
768
|
+
route: this.routeForStage('evaluation_repair'),
|
|
769
|
+
conditional: true,
|
|
770
|
+
system_prompt: FUSION_EVALUATION_REPAIR_SYSTEM_PROMPT,
|
|
771
|
+
empty_user_prompt: repairPrompt,
|
|
772
|
+
upstream_output_contract_bytes:
|
|
773
|
+
3 * FUSION_CANDIDATE_MAX_OUTPUT_BYTES +
|
|
774
|
+
FUSION_EVALUATION_MAX_OUTPUT_BYTES +
|
|
775
|
+
FUSION_DIAGNOSTICS_MAX_BYTES,
|
|
776
|
+
},
|
|
777
|
+
];
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
private entries(input: FusionCanonicalInputV3): readonly FusionStageBudgetPlanEntry[] {
|
|
781
|
+
return this.drafts(input).map(forecastEntry);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
private composition(
|
|
785
|
+
input: FusionCanonicalInputV3,
|
|
786
|
+
blocker: FusionBudgetBlocker,
|
|
787
|
+
): FusionBudgetStageComposition {
|
|
788
|
+
const canonicalBytes = utf8Bytes(buildCandidatePrompt(input));
|
|
789
|
+
const emptyRequestCanonicalBytes = utf8Bytes(buildCandidatePrompt(replaceRequestText(input, '')));
|
|
790
|
+
const requestBytes = canonicalBytes - emptyRequestCanonicalBytes;
|
|
791
|
+
const visible = visibleTextBytes(input);
|
|
792
|
+
const omissions = omissionReceiptBytes(input);
|
|
793
|
+
const projectionMetadata = canonicalBytes - requestBytes - visible - omissions;
|
|
794
|
+
const draft = this.drafts(input).find(
|
|
795
|
+
(item) => item.budget_stage === blocker.budget_stage && item.slot === blocker.slot,
|
|
796
|
+
);
|
|
797
|
+
if (draft === undefined) {
|
|
798
|
+
throw new FusionError('primary budget blocker disappeared during composition', {
|
|
799
|
+
code: 'orchestration_failed',
|
|
800
|
+
childCreated: false,
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
return {
|
|
804
|
+
visible_text_bytes: visible,
|
|
805
|
+
omission_receipt_bytes: omissions,
|
|
806
|
+
projection_metadata_bytes: projectionMetadata,
|
|
807
|
+
request_bytes: requestBytes,
|
|
808
|
+
static_stage_framing_bytes:
|
|
809
|
+
utf8Bytes(draft.system_prompt) + utf8Bytes(draft.empty_user_prompt) - canonicalBytes,
|
|
810
|
+
upstream_output_contract_bytes: draft.upstream_output_contract_bytes,
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
private emptyRequestVerdict(
|
|
815
|
+
input: FusionCanonicalInputV3,
|
|
816
|
+
entries: readonly FusionStageBudgetPlanEntry[],
|
|
817
|
+
): FusionBudgetEmptyRequestVerdict {
|
|
818
|
+
const emptyEntries = this.entries(replaceRequestText(input, ''));
|
|
819
|
+
const emptyBlockers = selectBlockers(emptyEntries);
|
|
820
|
+
let reduction = 0;
|
|
821
|
+
for (const entry of entries) {
|
|
822
|
+
reduction = Math.max(reduction, entry.input_utf8_bytes - maxKnownTextBytes(entry.route));
|
|
823
|
+
}
|
|
824
|
+
reduction = Math.max(0, reduction);
|
|
825
|
+
const requestBytes = utf8Bytes(input.request.text);
|
|
826
|
+
return {
|
|
827
|
+
request_utf8_bytes: requestBytes,
|
|
828
|
+
still_fails_with_empty_request: emptyBlockers.length > 0,
|
|
829
|
+
shortening_request_can_help: emptyBlockers.length === 0,
|
|
830
|
+
minimum_request_byte_reduction: reduction,
|
|
831
|
+
maximum_safe_request_utf8_bytes: Math.max(0, requestBytes - reduction),
|
|
832
|
+
blockers_with_empty_request: emptyBlockers,
|
|
833
|
+
};
|
|
274
834
|
}
|
|
275
835
|
|
|
276
836
|
private failure(
|
|
277
|
-
|
|
837
|
+
primary: FusionBudgetBlocker,
|
|
838
|
+
plan: FusionBudgetPlanV1,
|
|
839
|
+
artifactDir: string,
|
|
278
840
|
measurementKind: FusionBudgetErrorDetail['measurement_kind'],
|
|
279
|
-
|
|
280
|
-
allowedTokens: number,
|
|
281
|
-
label: string,
|
|
841
|
+
checkKind: FusionBudgetCheckKind,
|
|
282
842
|
): FusionError {
|
|
283
|
-
const
|
|
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;
|
|
284
847
|
const budget: FusionBudgetErrorDetail = {
|
|
285
|
-
budget_stage:
|
|
848
|
+
budget_stage: primary.budget_stage,
|
|
286
849
|
measurement_kind: measurementKind,
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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,
|
|
853
|
+
allowed_input_tokens: primary.allowed_input_tokens,
|
|
290
854
|
limiting_model: {
|
|
291
|
-
provider:
|
|
292
|
-
model:
|
|
293
|
-
qualified_id:
|
|
294
|
-
context_window_tokens:
|
|
855
|
+
provider: primary.route.provider,
|
|
856
|
+
model: primary.route.model,
|
|
857
|
+
qualified_id: primary.route.qualified_id,
|
|
858
|
+
context_window_tokens: primary.route.context_window_tokens,
|
|
295
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,
|
|
296
874
|
context_policy_id: this.contextPolicyId,
|
|
297
|
-
remediation
|
|
875
|
+
remediation,
|
|
876
|
+
blockers: plan.blockers,
|
|
877
|
+
artifact_dir: artifactDir,
|
|
298
878
|
};
|
|
879
|
+
if (primary.slot !== undefined) budget.slot = primary.slot;
|
|
880
|
+
const compositionText = composition === undefined ? 'unavailable' : formatComposition(composition);
|
|
881
|
+
const additional = plan.blockers
|
|
882
|
+
.filter((blocker) => blocker !== primary)
|
|
883
|
+
.map((blocker) => `${entryLabel(blocker)} route=${blocker.route.qualified_id}`)
|
|
884
|
+
.join('; ');
|
|
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}.`;
|
|
299
898
|
const message =
|
|
300
|
-
`
|
|
301
|
-
`
|
|
302
|
-
`
|
|
303
|
-
|
|
304
|
-
`
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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} ` +
|
|
902
|
+
`No child was created. Nothing was clipped, dropped, or substituted. Artifact directory: ${artifactDir}.\n` +
|
|
903
|
+
`Per-stage forecast table:\n${formatTable(plan.stages)}\n` +
|
|
904
|
+
`Primary blocker byte composition: ${compositionText}.\n` +
|
|
905
|
+
`Additional blockers: ${additional.length === 0 ? 'none' : additional}.\n` +
|
|
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` +
|
|
908
|
+
`Remediation: ${remediation.join(' ')}`;
|
|
909
|
+
const details = {
|
|
910
|
+
code: budgetErrorCode(checkKind),
|
|
310
911
|
childCreated: false,
|
|
311
912
|
budget,
|
|
312
|
-
|
|
913
|
+
stage: stageFromBudgetStage(primary.budget_stage),
|
|
914
|
+
};
|
|
915
|
+
if (primary.slot !== undefined) return new FusionError(message, { ...details, slot: primary.slot });
|
|
916
|
+
return new FusionError(message, details);
|
|
313
917
|
}
|
|
314
918
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
919
|
+
plan(input: FusionCanonicalInputV3): FusionBudgetPlanV1 {
|
|
920
|
+
const stages = this.entries(input);
|
|
921
|
+
const blockers = selectBlockers(stages);
|
|
922
|
+
const primary = selectPrimaryBlocker(blockers);
|
|
923
|
+
const emptyRequest = this.emptyRequestVerdict(input, stages);
|
|
924
|
+
const base: FusionBudgetPlanV1 = {
|
|
925
|
+
schema_version: FUSION_BUDGET_PLAN_SCHEMA_VERSION,
|
|
926
|
+
policy: FUSION_BUDGET_POLICY,
|
|
927
|
+
routes: this.routes,
|
|
928
|
+
stages,
|
|
929
|
+
blockers,
|
|
930
|
+
empty_request: emptyRequest,
|
|
931
|
+
warnings: warningsFor(stages),
|
|
932
|
+
};
|
|
933
|
+
if (primary === undefined) return base;
|
|
934
|
+
return {
|
|
935
|
+
...base,
|
|
936
|
+
primary_blocker: primary,
|
|
937
|
+
primary_blocker_composition: this.composition(input, primary),
|
|
938
|
+
};
|
|
332
939
|
}
|
|
333
940
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
* that will be written to the child's stdin and persisted as the artifact.
|
|
337
|
-
*/
|
|
338
|
-
assertStagePrompt(stage: FusionBudgetStage, systemPrompt: string, userPrompt: string): void {
|
|
339
|
-
const bytes =
|
|
340
|
-
Buffer.byteLength(systemPrompt, 'utf8') + Buffer.byteLength(userPrompt, 'utf8');
|
|
341
|
-
if (fusionTokenUpperBound(bytes) > this.allowedInputTokens) {
|
|
941
|
+
assertPlanFits(plan: FusionBudgetPlanV1, artifactDir: string): void {
|
|
942
|
+
if (plan.primary_blocker !== undefined) {
|
|
342
943
|
throw this.failure(
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
944
|
+
plan.primary_blocker,
|
|
945
|
+
plan,
|
|
946
|
+
artifactDir,
|
|
947
|
+
'stage_forecast',
|
|
948
|
+
'input_only_preflight',
|
|
348
949
|
);
|
|
349
950
|
}
|
|
350
951
|
}
|
|
351
952
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
953
|
+
assertStagePrompt(
|
|
954
|
+
stage: FusionBudgetStage,
|
|
955
|
+
systemPrompt: string,
|
|
956
|
+
userPrompt: string,
|
|
957
|
+
slot?: 1 | 2 | 3,
|
|
958
|
+
): void {
|
|
959
|
+
const route = this.routeForStage(stage, slot);
|
|
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;
|
|
964
|
+
const entry: FusionStageBudgetPlanEntry = {
|
|
965
|
+
budget_stage: stage,
|
|
966
|
+
route,
|
|
967
|
+
conditional: stage === 'evaluation_repair',
|
|
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,
|
|
974
|
+
allowed_input_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,
|
|
981
|
+
fits: false,
|
|
982
|
+
reservation_fits: false,
|
|
363
983
|
};
|
|
364
|
-
|
|
984
|
+
if (slot !== undefined) entry.slot = slot;
|
|
985
|
+
const blocker = blockerFromEntry(entry);
|
|
986
|
+
const plan: FusionBudgetPlanV1 = {
|
|
365
987
|
schema_version: FUSION_BUDGET_PLAN_SCHEMA_VERSION,
|
|
366
988
|
policy: FUSION_BUDGET_POLICY,
|
|
367
989
|
routes: this.routes,
|
|
368
|
-
|
|
369
|
-
|
|
990
|
+
stages: [entry],
|
|
991
|
+
blockers: [blocker],
|
|
992
|
+
primary_blocker: blocker,
|
|
993
|
+
empty_request: {
|
|
994
|
+
request_utf8_bytes: 0,
|
|
995
|
+
still_fails_with_empty_request: true,
|
|
996
|
+
shortening_request_can_help: false,
|
|
997
|
+
minimum_request_byte_reduction: blocker.bytes_over,
|
|
998
|
+
maximum_safe_request_utf8_bytes: 0,
|
|
999
|
+
blockers_with_empty_request: [blocker],
|
|
1000
|
+
},
|
|
1001
|
+
warnings: [],
|
|
1002
|
+
};
|
|
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,
|
|
370
1042
|
};
|
|
1043
|
+
if (slot !== undefined) violation.slot = slot;
|
|
1044
|
+
return violation;
|
|
371
1045
|
}
|
|
372
1046
|
}
|