pi-background-tasks 1.0.4 → 1.0.6
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/BACKGROUND-TASKS-INSTRUCTIONS.md +2 -1
- package/README.md +2 -1
- package/TESTING.md +2 -2
- package/TEST_PLAN.md +10 -9
- package/docs/manifest.json +8 -4
- package/docs/operations/configuration.md +10 -0
- package/docs/operations/testing.md +2 -1
- package/docs/operations/troubleshooting.md +3 -1
- package/docs/read-before-edit.md +1 -0
- package/docs/reference/runtime-contracts.md +44 -40
- package/docs/subsystems/docs-freshness-gate.md +5 -5
- package/docs/subsystems/fusion.md +17 -12
- package/package.json +2 -1
- package/src/core/fusion/budget.ts +11 -4
- package/src/core/fusion/child-protocol.ts +137 -12
- package/src/core/fusion/claude-cache.ts +186 -0
- package/src/core/fusion/config.ts +13 -0
- package/src/core/fusion/pi-child.ts +558 -14
- package/src/core/fusion/types.ts +29 -7
- package/src/fusion-child-extension.ts +413 -4
|
@@ -88,7 +88,8 @@ export const FUSION_MIN_CONTEXT_WINDOW_TOKENS =
|
|
|
88
88
|
FUSION_SAFETY_RESERVE_TOKENS;
|
|
89
89
|
|
|
90
90
|
export const FUSION_BUDGET_POLICY: FusionBudgetPolicyDescriptor = {
|
|
91
|
-
id: 'fusion-budget-policy-
|
|
91
|
+
id: 'fusion-budget-policy-v4',
|
|
92
|
+
route_output_reserve_strategy: 'max_fusion_contract_or_model_max',
|
|
92
93
|
calibration_version: TOKEN_BUDGET_CALIBRATION_VERSION,
|
|
93
94
|
calibration_table: FUSION_CALIBRATED_BYTES_PER_TOKEN,
|
|
94
95
|
reserved_output_tokens: FUSION_RESERVED_OUTPUT_TOKENS,
|
|
@@ -288,14 +289,20 @@ function routeCapacity(
|
|
|
288
289
|
role: FusionRouteCapacity['role'],
|
|
289
290
|
): FusionRouteCapacity {
|
|
290
291
|
const contextWindow = requirePositiveContextWindow(model, role);
|
|
292
|
+
const reservedOutputTokens = Math.max(FUSION_RESERVED_OUTPUT_TOKENS, model.maxOutputTokens);
|
|
291
293
|
const allowed = allowedInputTokens(contextWindow, {
|
|
292
|
-
reservedOutputTokens
|
|
294
|
+
reservedOutputTokens,
|
|
293
295
|
framingReserveTokens: FUSION_FRAMING_RESERVE_TOKENS,
|
|
294
296
|
safetyReserveTokens: FUSION_SAFETY_RESERVE_TOKENS,
|
|
295
297
|
});
|
|
296
298
|
if (allowed < FUSION_MIN_CANONICAL_INPUT_TOKENS) {
|
|
299
|
+
const minimumContextWindow =
|
|
300
|
+
reservedOutputTokens +
|
|
301
|
+
FUSION_FRAMING_RESERVE_TOKENS +
|
|
302
|
+
FUSION_SAFETY_RESERVE_TOKENS +
|
|
303
|
+
FUSION_MIN_CANONICAL_INPUT_TOKENS;
|
|
297
304
|
throw new FusionError(
|
|
298
|
-
`fusion ${role} route ${model.qualifiedId} has a ${String(contextWindow)}-token context window, but Fusion requires at least ${String(
|
|
305
|
+
`fusion ${role} route ${model.qualifiedId} has a ${String(contextWindow)}-token context window, but Fusion requires at least ${String(minimumContextWindow)} tokens: ${String(reservedOutputTokens)} reserved for the route's configured maximum 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 or lower-max-output subscription model for this slot with /fusion-models.`,
|
|
299
306
|
{ code: 'model_capacity_unknown', childCreated: false },
|
|
300
307
|
);
|
|
301
308
|
}
|
|
@@ -322,7 +329,7 @@ function routeCapacity(
|
|
|
322
329
|
model: model.model,
|
|
323
330
|
qualified_id: model.qualifiedId,
|
|
324
331
|
context_window_tokens: contextWindow,
|
|
325
|
-
reserved_output_tokens:
|
|
332
|
+
reserved_output_tokens: reservedOutputTokens,
|
|
326
333
|
framing_reserve_tokens: FUSION_FRAMING_RESERVE_TOKENS,
|
|
327
334
|
safety_reserve_tokens: FUSION_SAFETY_RESERVE_TOKENS,
|
|
328
335
|
allowed_input_tokens: allowed,
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import type { Usage } from '@earendil-works/pi-ai';
|
|
3
|
+
import type { FusionClaudeCacheObservation } from './claude-cache.js';
|
|
3
4
|
|
|
4
5
|
export const FUSION_CHILD_RESULT_SCHEMA_VERSION =
|
|
5
|
-
'pi-background-tasks.fusion-child-result.
|
|
6
|
+
'pi-background-tasks.fusion-child-result.v3' as const;
|
|
6
7
|
export const FUSION_CHILD_RESULT_PREFIX = '\u001ePI_FUSION_CHILD_RESULT ';
|
|
8
|
+
export const FUSION_CHILD_SETTLEMENT_SCHEMA_VERSION =
|
|
9
|
+
'pi-background-tasks.fusion-child-settlement.v2' as const;
|
|
10
|
+
export const FUSION_CHILD_SETTLEMENT_PREFIX = '\u001ePI_FUSION_CHILD_SETTLEMENT ';
|
|
7
11
|
export const FUSION_TOOL_CALL_LOG_PATH_ENV = 'PI_FUSION_TOOL_CALL_LOG_PATH';
|
|
8
12
|
export const FUSION_RESEARCH_ENABLED_ENV = 'PI_FUSION_RESEARCH_ENABLED';
|
|
9
13
|
export const FUSION_SOURCE_POLICY_PATH_ENV = 'PI_FUSION_SOURCE_POLICY_PATH';
|
|
@@ -11,17 +15,47 @@ export const FUSION_SOURCE_POLICY_SHA256_ENV = 'PI_FUSION_SOURCE_POLICY_SHA256';
|
|
|
11
15
|
export const FUSION_TOOL_CALL_SEAL_SCHEMA_VERSION =
|
|
12
16
|
'pi-background-tasks.fusion-tool-call-seal.v1' as const;
|
|
13
17
|
export const FUSION_TOOL_CALL_SEAL_SUFFIX = '.seal.json';
|
|
18
|
+
export const FUSION_RUNTIME_GUARD_SCHEMA_VERSION =
|
|
19
|
+
'pi-background-tasks.fusion-runtime-guard.v1' as const;
|
|
20
|
+
export const FUSION_RUNTIME_GUARD_PREFIX = '\u001ePI_FUSION_RUNTIME_GUARD ';
|
|
21
|
+
export const FUSION_CHILD_MAX_PROVIDER_REQUESTS = 128;
|
|
22
|
+
export const FUSION_CHILD_MAX_TOOL_CALLS = 192;
|
|
23
|
+
export const FUSION_CHILD_MIN_OUTPUT_RESERVE_TOKENS = 32_768;
|
|
24
|
+
export const FUSION_CHILD_SAFETY_RESERVE_TOKENS = 4_096;
|
|
14
25
|
|
|
15
26
|
/**
|
|
16
27
|
* Aggregate ceiling on tool-result bytes a single candidate child may accumulate.
|
|
17
28
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* degrading into an opaque provider-side context failure.
|
|
29
|
+
* The byte ceiling complements the runtime provider-payload governor and tool/request
|
|
30
|
+
* count limits. It remains an independent bound on total tool material even when Pi
|
|
31
|
+
* compaction keeps each individual provider request within the route context window.
|
|
22
32
|
*/
|
|
23
33
|
export const FUSION_CHILD_MAX_TOTAL_TOOL_RESULT_BYTES = 8 * 1024 * 1024;
|
|
24
34
|
|
|
35
|
+
export type FusionRuntimeGuardCode =
|
|
36
|
+
| 'provider_request_limit'
|
|
37
|
+
| 'provider_request_budget'
|
|
38
|
+
| 'provider_payload_invalid'
|
|
39
|
+
| 'claude_cache_policy'
|
|
40
|
+
| 'tool_call_limit';
|
|
41
|
+
|
|
42
|
+
export interface FusionRuntimeGuardRecord {
|
|
43
|
+
schema_version: typeof FUSION_RUNTIME_GUARD_SCHEMA_VERSION;
|
|
44
|
+
code: FusionRuntimeGuardCode;
|
|
45
|
+
provider: string;
|
|
46
|
+
model: string;
|
|
47
|
+
request_ordinal: number;
|
|
48
|
+
tool_call_count: number;
|
|
49
|
+
payload_bytes: number;
|
|
50
|
+
payload_sha256: string;
|
|
51
|
+
estimated_input_tokens: number;
|
|
52
|
+
context_window_tokens: number;
|
|
53
|
+
reserved_output_tokens: number;
|
|
54
|
+
safety_reserve_tokens: number;
|
|
55
|
+
allowed_input_tokens: number;
|
|
56
|
+
message: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
25
59
|
export interface FusionChildTextBlockMetadata {
|
|
26
60
|
utf8_bytes: number;
|
|
27
61
|
sha256: string;
|
|
@@ -37,19 +71,109 @@ export interface FusionChildResultMetadata {
|
|
|
37
71
|
text_blocks: FusionChildTextBlockMetadata[];
|
|
38
72
|
text_sha256: string;
|
|
39
73
|
usage: FusionChildResultUsageMetadata;
|
|
74
|
+
cache_observation: FusionClaudeCacheObservation;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type FusionChildSettlementFailureReason =
|
|
78
|
+
| 'no_records'
|
|
79
|
+
| 'final_not_stop'
|
|
80
|
+
| 'invalid_non_final'
|
|
81
|
+
| 'runtime_guard'
|
|
82
|
+
| 'cache_observation';
|
|
83
|
+
|
|
84
|
+
export interface FusionChildSettlementRecord {
|
|
85
|
+
schema_version: typeof FUSION_CHILD_SETTLEMENT_SCHEMA_VERSION;
|
|
86
|
+
status: 'complete' | 'failed';
|
|
87
|
+
record_count: number;
|
|
88
|
+
records_sha256: string;
|
|
89
|
+
final_record_index: number | null;
|
|
90
|
+
final_text_sha256: string | null;
|
|
91
|
+
recovered_error_ordinals: number[];
|
|
92
|
+
failure_reason: FusionChildSettlementFailureReason | null;
|
|
40
93
|
}
|
|
41
94
|
|
|
42
95
|
function protocolSha256(value: string | Buffer): string {
|
|
43
96
|
return createHash('sha256').update(value).digest('hex');
|
|
44
97
|
}
|
|
45
98
|
|
|
46
|
-
export function
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
99
|
+
export function serializeFusionChildResultRecords(
|
|
100
|
+
records: readonly FusionChildResultMetadata[],
|
|
101
|
+
): Buffer {
|
|
102
|
+
return Buffer.from(
|
|
103
|
+
records.length === 0 ? '' : `${records.map((record) => JSON.stringify(record)).join('\n')}\n`,
|
|
104
|
+
'utf8',
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function hasZeroUsage(record: FusionChildResultMetadata): boolean {
|
|
109
|
+
const usage = record.usage;
|
|
110
|
+
return (
|
|
111
|
+
usage.input === 0 &&
|
|
112
|
+
usage.output === 0 &&
|
|
113
|
+
usage.cacheRead === 0 &&
|
|
114
|
+
usage.cacheWrite === 0 &&
|
|
115
|
+
usage.totalTokens === 0 &&
|
|
116
|
+
usage.cost.input === 0 &&
|
|
117
|
+
usage.cost.output === 0 &&
|
|
118
|
+
usage.cost.cacheRead === 0 &&
|
|
119
|
+
usage.cost.cacheWrite === 0 &&
|
|
120
|
+
usage.cost.total === 0
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function isRecoverableFusionChildErrorRecord(record: FusionChildResultMetadata): boolean {
|
|
125
|
+
return (
|
|
126
|
+
record.stop_reason === 'error' &&
|
|
127
|
+
record.text_blocks.length === 0 &&
|
|
128
|
+
record.text_sha256 === protocolSha256(Buffer.alloc(0)) &&
|
|
129
|
+
hasZeroUsage(record)
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function buildFusionChildSettlement(
|
|
134
|
+
records: readonly FusionChildResultMetadata[],
|
|
135
|
+
runtimeGuardFailed = false,
|
|
136
|
+
cacheObservationFailed = false,
|
|
137
|
+
): FusionChildSettlementRecord {
|
|
138
|
+
const finalRecordIndex = records.length === 0 ? null : records.length - 1;
|
|
139
|
+
const final = records.at(-1);
|
|
140
|
+
const recoveredErrorOrdinals = records.flatMap((record, ordinal) =>
|
|
141
|
+
ordinal < records.length - 1 && isRecoverableFusionChildErrorRecord(record) ? [ordinal] : [],
|
|
142
|
+
);
|
|
143
|
+
const invalidNonFinal = records.some(
|
|
144
|
+
(record, ordinal) =>
|
|
145
|
+
ordinal < records.length - 1 &&
|
|
146
|
+
record.stop_reason !== 'toolUse' &&
|
|
147
|
+
!isRecoverableFusionChildErrorRecord(record),
|
|
148
|
+
);
|
|
149
|
+
let failureReason: FusionChildSettlementFailureReason | null = null;
|
|
150
|
+
if (runtimeGuardFailed) failureReason = 'runtime_guard';
|
|
151
|
+
else if (cacheObservationFailed) failureReason = 'cache_observation';
|
|
152
|
+
else if (final === undefined) failureReason = 'no_records';
|
|
153
|
+
else if (final.stop_reason !== 'stop') failureReason = 'final_not_stop';
|
|
154
|
+
else if (invalidNonFinal) failureReason = 'invalid_non_final';
|
|
155
|
+
return {
|
|
156
|
+
schema_version: FUSION_CHILD_SETTLEMENT_SCHEMA_VERSION,
|
|
157
|
+
status: failureReason === null ? 'complete' : 'failed',
|
|
158
|
+
record_count: records.length,
|
|
159
|
+
records_sha256: protocolSha256(serializeFusionChildResultRecords(records)),
|
|
160
|
+
final_record_index: finalRecordIndex,
|
|
161
|
+
final_text_sha256: final?.text_sha256 ?? null,
|
|
162
|
+
recovered_error_ordinals: recoveredErrorOrdinals,
|
|
163
|
+
failure_reason: failureReason,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function buildFusionChildResultMetadata(
|
|
168
|
+
message: {
|
|
169
|
+
provider: string;
|
|
170
|
+
model: string;
|
|
171
|
+
stopReason: string;
|
|
172
|
+
content: ReadonlyArray<{ type: string; text?: string }>;
|
|
173
|
+
usage: Usage;
|
|
174
|
+
},
|
|
175
|
+
cacheObservation: FusionClaudeCacheObservation,
|
|
176
|
+
): FusionChildResultMetadata {
|
|
53
177
|
const textBlocks = message.content.flatMap((part) =>
|
|
54
178
|
part.type === 'text' && typeof part.text === 'string' ? [part.text] : [],
|
|
55
179
|
);
|
|
@@ -78,5 +202,6 @@ export function buildFusionChildResultMetadata(message: {
|
|
|
78
202
|
})),
|
|
79
203
|
text_sha256: protocolSha256(textBlocks.join('')),
|
|
80
204
|
usage,
|
|
205
|
+
cache_observation: cacheObservation,
|
|
81
206
|
};
|
|
82
207
|
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { JsonObject } from '../common.js';
|
|
2
|
+
|
|
3
|
+
export const FUSION_CLAUDE_CACHE_OBSERVATION_SCHEMA_VERSION =
|
|
4
|
+
'pi-background-tasks.fusion-claude-cache-observation.v1' as const;
|
|
5
|
+
export const FUSION_CLAUDE_CACHE_RETENTION_ENV = 'PI_CACHE_RETENTION';
|
|
6
|
+
export const FUSION_CLAUDE_CACHE_DEFAULT_RETENTION = 'long' as const;
|
|
7
|
+
export const FUSION_CLAUDE_CACHE_BREAKPOINT_LIMIT = 4;
|
|
8
|
+
|
|
9
|
+
export type FusionClaudeCacheRetention = 'none' | 'short' | 'long';
|
|
10
|
+
export type FusionClaudeCachePolicySource =
|
|
11
|
+
| 'default'
|
|
12
|
+
| typeof FUSION_CLAUDE_CACHE_RETENTION_ENV
|
|
13
|
+
| 'not_applicable';
|
|
14
|
+
|
|
15
|
+
export interface FusionClaudeCacheObservation {
|
|
16
|
+
schema_version: typeof FUSION_CLAUDE_CACHE_OBSERVATION_SCHEMA_VERSION;
|
|
17
|
+
applicability: 'anthropic' | 'not_applicable';
|
|
18
|
+
source: FusionClaudeCachePolicySource;
|
|
19
|
+
requested_retention: FusionClaudeCacheRetention | null;
|
|
20
|
+
effective_retention: FusionClaudeCacheRetention | null;
|
|
21
|
+
breakpoint_count: number;
|
|
22
|
+
request_ordinal: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface FusionClaudeCacheNormalization {
|
|
26
|
+
payload: JsonObject;
|
|
27
|
+
observation: FusionClaudeCacheObservation;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isRecord(value: unknown): value is JsonObject {
|
|
31
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function unknownArray(value: unknown): unknown[] | undefined {
|
|
35
|
+
return Array.isArray(value) ? (value as unknown[]) : undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function requireRequestOrdinal(value: number): number {
|
|
39
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
40
|
+
throw new Error('Fusion Claude cache request ordinal must be a positive safe integer');
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseRetention(value: string): FusionClaudeCacheRetention {
|
|
46
|
+
if (value === 'none' || value === 'short' || value === 'long') return value;
|
|
47
|
+
throw new Error(
|
|
48
|
+
`${FUSION_CLAUDE_CACHE_RETENTION_ENV} must be one of none, short, or long; got ${JSON.stringify(value)}`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function resolveFusionClaudeCachePolicy(env: Readonly<NodeJS.ProcessEnv> = process.env): {
|
|
53
|
+
retention: FusionClaudeCacheRetention;
|
|
54
|
+
source: FusionClaudeCachePolicySource;
|
|
55
|
+
} {
|
|
56
|
+
const configured = env[FUSION_CLAUDE_CACHE_RETENTION_ENV];
|
|
57
|
+
if (configured === undefined) {
|
|
58
|
+
return { retention: FUSION_CLAUDE_CACHE_DEFAULT_RETENTION, source: 'default' };
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
retention: parseRetention(configured),
|
|
62
|
+
source: FUSION_CLAUDE_CACHE_RETENTION_ENV,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function validateCacheControl(value: unknown): JsonObject {
|
|
67
|
+
if (!isRecord(value)) {
|
|
68
|
+
throw new Error('Fusion Claude cache_control must be an object');
|
|
69
|
+
}
|
|
70
|
+
if (value['type'] !== 'ephemeral') {
|
|
71
|
+
throw new Error('Fusion Claude cache_control.type must be "ephemeral"');
|
|
72
|
+
}
|
|
73
|
+
const ttl = value['ttl'];
|
|
74
|
+
if (ttl !== undefined && ttl !== '1h' && ttl !== '5m') {
|
|
75
|
+
throw new Error('Fusion Claude cache_control.ttl must be "1h" or "5m" when present');
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Normalize only cache breakpoints already selected by Pi's Anthropic adapter.
|
|
82
|
+
*
|
|
83
|
+
* Not creating new breakpoints is deliberate: an empty marker set may represent
|
|
84
|
+
* Pi's explicit cacheRetention="none" compaction request or a model compatibility
|
|
85
|
+
* restriction. The package may strengthen or disable native markers, but it must
|
|
86
|
+
* not override an upstream call-level opt-out that is no longer visible in the
|
|
87
|
+
* final provider payload.
|
|
88
|
+
*/
|
|
89
|
+
export function normalizeFusionClaudeCachePayload(input: {
|
|
90
|
+
payload: unknown;
|
|
91
|
+
requestOrdinal: number;
|
|
92
|
+
env?: Readonly<NodeJS.ProcessEnv>;
|
|
93
|
+
supportsLongCacheRetention?: boolean | undefined;
|
|
94
|
+
}): FusionClaudeCacheNormalization {
|
|
95
|
+
if (!isRecord(input.payload)) {
|
|
96
|
+
throw new Error('Fusion Claude provider payload must be an object');
|
|
97
|
+
}
|
|
98
|
+
const requestOrdinal = requireRequestOrdinal(input.requestOrdinal);
|
|
99
|
+
const policy = resolveFusionClaudeCachePolicy(input.env ?? process.env);
|
|
100
|
+
const normalizedRetention: FusionClaudeCacheRetention =
|
|
101
|
+
policy.retention === 'long' && input.supportsLongCacheRetention === false
|
|
102
|
+
? 'short'
|
|
103
|
+
: policy.retention;
|
|
104
|
+
let incomingBreakpoints = 0;
|
|
105
|
+
let outputBreakpoints = 0;
|
|
106
|
+
|
|
107
|
+
const normalizeBlock = (value: unknown): unknown => {
|
|
108
|
+
if (!isRecord(value) || !Object.hasOwn(value, 'cache_control')) return value;
|
|
109
|
+
const existing = value['cache_control'];
|
|
110
|
+
if (existing === undefined) {
|
|
111
|
+
const next = { ...value };
|
|
112
|
+
Reflect.deleteProperty(next, 'cache_control');
|
|
113
|
+
return next;
|
|
114
|
+
}
|
|
115
|
+
incomingBreakpoints += 1;
|
|
116
|
+
if (incomingBreakpoints > FUSION_CLAUDE_CACHE_BREAKPOINT_LIMIT) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`Fusion Claude payload has ${String(incomingBreakpoints)} cache_control breakpoints; Anthropic supports at most ${String(FUSION_CLAUDE_CACHE_BREAKPOINT_LIMIT)}`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
const control = validateCacheControl(existing);
|
|
122
|
+
const next = { ...value };
|
|
123
|
+
if (normalizedRetention === 'none') {
|
|
124
|
+
Reflect.deleteProperty(next, 'cache_control');
|
|
125
|
+
return next;
|
|
126
|
+
}
|
|
127
|
+
const normalizedControl = { ...control, type: 'ephemeral' };
|
|
128
|
+
Reflect.deleteProperty(normalizedControl, 'ttl');
|
|
129
|
+
if (normalizedRetention === 'long') Object.assign(normalizedControl, { ttl: '1h' });
|
|
130
|
+
next['cache_control'] = normalizedControl;
|
|
131
|
+
outputBreakpoints += 1;
|
|
132
|
+
return next;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const system = unknownArray(input.payload['system']);
|
|
136
|
+
const tools = unknownArray(input.payload['tools']);
|
|
137
|
+
const messages = unknownArray(input.payload['messages']);
|
|
138
|
+
const payload = {
|
|
139
|
+
...input.payload,
|
|
140
|
+
...(system === undefined ? {} : { system: system.map(normalizeBlock) }),
|
|
141
|
+
...(tools === undefined ? {} : { tools: tools.map(normalizeBlock) }),
|
|
142
|
+
...(messages === undefined
|
|
143
|
+
? {}
|
|
144
|
+
: {
|
|
145
|
+
messages: messages.map((message) => {
|
|
146
|
+
if (!isRecord(message)) return message;
|
|
147
|
+
const content = unknownArray(message['content']);
|
|
148
|
+
return content === undefined
|
|
149
|
+
? message
|
|
150
|
+
: { ...message, content: content.map(normalizeBlock) };
|
|
151
|
+
}),
|
|
152
|
+
}),
|
|
153
|
+
};
|
|
154
|
+
if (outputBreakpoints > FUSION_CLAUDE_CACHE_BREAKPOINT_LIMIT) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`Fusion Claude payload produced ${String(outputBreakpoints)} cache_control breakpoints; Anthropic supports at most ${String(FUSION_CLAUDE_CACHE_BREAKPOINT_LIMIT)}`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
payload,
|
|
162
|
+
observation: {
|
|
163
|
+
schema_version: FUSION_CLAUDE_CACHE_OBSERVATION_SCHEMA_VERSION,
|
|
164
|
+
applicability: 'anthropic',
|
|
165
|
+
source: policy.source,
|
|
166
|
+
requested_retention: policy.retention,
|
|
167
|
+
effective_retention: outputBreakpoints === 0 ? 'none' : normalizedRetention,
|
|
168
|
+
breakpoint_count: outputBreakpoints,
|
|
169
|
+
request_ordinal: requestOrdinal,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function nonAnthropicFusionCacheObservation(
|
|
175
|
+
requestOrdinal: number,
|
|
176
|
+
): FusionClaudeCacheObservation {
|
|
177
|
+
return {
|
|
178
|
+
schema_version: FUSION_CLAUDE_CACHE_OBSERVATION_SCHEMA_VERSION,
|
|
179
|
+
applicability: 'not_applicable',
|
|
180
|
+
source: 'not_applicable',
|
|
181
|
+
requested_retention: null,
|
|
182
|
+
effective_retention: null,
|
|
183
|
+
breakpoint_count: 0,
|
|
184
|
+
request_ordinal: requireRequestOrdinal(requestOrdinal),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
@@ -158,6 +158,17 @@ function requireContextWindow(model: Model<Api>, label: string): number {
|
|
|
158
158
|
return Math.floor(value);
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
function requireMaxOutputTokens(model: Model<Api>, label: string): number {
|
|
162
|
+
const value = model.maxTokens;
|
|
163
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
|
164
|
+
throw new FusionError(`${label} has no positive maximum output token capacity`, {
|
|
165
|
+
code: 'model_unavailable',
|
|
166
|
+
childCreated: false,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return Math.floor(value);
|
|
170
|
+
}
|
|
171
|
+
|
|
161
172
|
function modelIndex(models: readonly Model<Api>[]): Map<string, Model<Api>> {
|
|
162
173
|
const out = new Map<string, Model<Api>>();
|
|
163
174
|
for (const model of models) out.set(qualifiedModelKey(model), model);
|
|
@@ -305,6 +316,7 @@ function resolveSelection(
|
|
|
305
316
|
qualifiedId,
|
|
306
317
|
thinkingLevel,
|
|
307
318
|
contextWindow: requireContextWindow(available, slotLabel),
|
|
319
|
+
maxOutputTokens: requireMaxOutputTokens(available, slotLabel),
|
|
308
320
|
};
|
|
309
321
|
}
|
|
310
322
|
const model = availableByKey.get(selection);
|
|
@@ -323,6 +335,7 @@ function resolveSelection(
|
|
|
323
335
|
qualifiedId: selection,
|
|
324
336
|
thinkingLevel,
|
|
325
337
|
contextWindow: requireContextWindow(model, slotLabel),
|
|
338
|
+
maxOutputTokens: requireMaxOutputTokens(model, slotLabel),
|
|
326
339
|
};
|
|
327
340
|
}
|
|
328
341
|
|