@nijaru/pi-provider-compaction 0.0.7 → 0.0.8
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/dist/index.js +56 -15
- package/index.ts +40 -14
- package/package.json +1 -1
- package/protocol.ts +24 -3
- package/provider.ts +5 -0
package/dist/index.js
CHANGED
|
@@ -200,6 +200,8 @@ function installObserver(pi, ctx, observe) {
|
|
|
200
200
|
const id = model.provider;
|
|
201
201
|
const native = registry.getRegisteredNativeProvider(id);
|
|
202
202
|
if (native) {
|
|
203
|
+
if (typeof native.getModels !== "function" || !native.getModels().some((entry) => entry.api === model.api))
|
|
204
|
+
return;
|
|
203
205
|
const isInstalled = () => registry.getRegisteredNativeProvider(id) === wrapper;
|
|
204
206
|
const guarded = (...args) => {
|
|
205
207
|
if (!isInstalled())
|
|
@@ -1228,6 +1230,15 @@ async function collectProviderUsage(stream, signal) {
|
|
|
1228
1230
|
throw new Error("Native compaction provider stream ended without usage");
|
|
1229
1231
|
return usage;
|
|
1230
1232
|
}
|
|
1233
|
+
|
|
1234
|
+
class NativeCompactionValidationError extends Error {
|
|
1235
|
+
usage;
|
|
1236
|
+
constructor(message, usage) {
|
|
1237
|
+
super(message);
|
|
1238
|
+
this.usage = usage;
|
|
1239
|
+
this.name = "NativeCompactionValidationError";
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1231
1242
|
async function requestResponsesCompact(request) {
|
|
1232
1243
|
let preparedPayload;
|
|
1233
1244
|
let compacted;
|
|
@@ -1254,9 +1265,13 @@ async function requestResponsesCompact(request) {
|
|
|
1254
1265
|
if (!response.ok)
|
|
1255
1266
|
return response;
|
|
1256
1267
|
const parsed = await readJsonBounded(response, request.signal);
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1268
|
+
try {
|
|
1269
|
+
if (!isObject(parsed))
|
|
1270
|
+
throw new Error("Responses Compact returned a non-object response");
|
|
1271
|
+
compacted = { response: parsed, output: validateCompactedOutput(parsed) };
|
|
1272
|
+
} catch (error) {
|
|
1273
|
+
throw new NativeCompactionValidationError(error instanceof Error ? error.message : String(error));
|
|
1274
|
+
}
|
|
1260
1275
|
return syntheticCompletion(parsed, preparedPayload);
|
|
1261
1276
|
} catch (error) {
|
|
1262
1277
|
bridgeError = error;
|
|
@@ -1268,6 +1283,7 @@ async function requestResponsesCompact(request) {
|
|
|
1268
1283
|
headers: request.headers,
|
|
1269
1284
|
env: request.env,
|
|
1270
1285
|
signal: request.signal,
|
|
1286
|
+
...request.serviceTier === undefined ? {} : { serviceTier: request.serviceTier },
|
|
1271
1287
|
transport: "sse",
|
|
1272
1288
|
cacheRetention: "none",
|
|
1273
1289
|
maxRetries: 0,
|
|
@@ -1286,6 +1302,8 @@ async function requestResponsesCompact(request) {
|
|
|
1286
1302
|
throw bridgeError;
|
|
1287
1303
|
throw error;
|
|
1288
1304
|
});
|
|
1305
|
+
if (bridgeError instanceof NativeCompactionValidationError)
|
|
1306
|
+
throw new NativeCompactionValidationError(bridgeError.message, usage);
|
|
1289
1307
|
if (bridgeError)
|
|
1290
1308
|
throw bridgeError;
|
|
1291
1309
|
if (!compacted || dispatches !== 1)
|
|
@@ -1430,6 +1448,7 @@ async function requestRemoteV2(request) {
|
|
|
1430
1448
|
headers: request.headers,
|
|
1431
1449
|
env: request.env,
|
|
1432
1450
|
signal: request.signal,
|
|
1451
|
+
...request.serviceTier === undefined ? {} : { serviceTier: request.serviceTier },
|
|
1433
1452
|
transport: "sse",
|
|
1434
1453
|
cacheRetention: "none",
|
|
1435
1454
|
maxRetries: 0,
|
|
@@ -1457,7 +1476,11 @@ async function requestRemoteV2(request) {
|
|
|
1457
1476
|
}
|
|
1458
1477
|
async function requestProviderCompaction(request) {
|
|
1459
1478
|
const result = await (request.protocol === "remote-v2" ? requestRemoteV2(request) : requestResponsesCompact(request));
|
|
1460
|
-
|
|
1479
|
+
try {
|
|
1480
|
+
return { ...result, output: validateCompactedOutput(result) };
|
|
1481
|
+
} catch (error) {
|
|
1482
|
+
throw new NativeCompactionValidationError(error instanceof Error ? error.message : String(error), result.usage);
|
|
1483
|
+
}
|
|
1461
1484
|
}
|
|
1462
1485
|
|
|
1463
1486
|
// replay.ts
|
|
@@ -1776,7 +1799,8 @@ Last attempt: ${compactionStatus}`, "info");
|
|
|
1776
1799
|
return;
|
|
1777
1800
|
}
|
|
1778
1801
|
if (!options.onPayload) {
|
|
1779
|
-
|
|
1802
|
+
if (captureStatus !== "eligible prepared request captured")
|
|
1803
|
+
captureRejected("no payload callback");
|
|
1780
1804
|
return;
|
|
1781
1805
|
}
|
|
1782
1806
|
if (generic(owner)) {
|
|
@@ -1795,7 +1819,7 @@ Last attempt: ${compactionStatus}`, "info");
|
|
|
1795
1819
|
if (generation !== ticket || ctx !== owner || options.signal?.aborted || !supportedRouteOptions(model, options))
|
|
1796
1820
|
return false;
|
|
1797
1821
|
try {
|
|
1798
|
-
return
|
|
1822
|
+
return unchanged(owner.sessionManager.getBranch(), branch.length, branchHash);
|
|
1799
1823
|
} catch {
|
|
1800
1824
|
retire();
|
|
1801
1825
|
return false;
|
|
@@ -1827,8 +1851,9 @@ Last attempt: ${compactionStatus}`, "info");
|
|
|
1827
1851
|
const routeHash = route(model, options);
|
|
1828
1852
|
const policyHash = digest(requestPolicy(model, prepared));
|
|
1829
1853
|
const latest = latestCompaction(branch);
|
|
1854
|
+
const payloadModel = isObject(payload) && typeof payload.model === "string" ? payload.model : undefined;
|
|
1830
1855
|
let result = payload;
|
|
1831
|
-
if (active && latest?.id === active.entryId && latest.summary === active.summary &&
|
|
1856
|
+
if (active && latest?.id === active.entryId && latest.summary === active.summary && active.payloadModel === payloadModel && unchanged(branch, active.branchLength, active.branchHash) && active.routeHash === routeHash && active.policyHash === policyHash) {
|
|
1832
1857
|
const details = readNativeCompactionDetails(latest.details);
|
|
1833
1858
|
const projection = owner.sessionManager.buildSessionProjection();
|
|
1834
1859
|
const contribution = projection.entries.find((entry) => entry.sourceEntry.id === latest.id);
|
|
@@ -1851,7 +1876,7 @@ Last attempt: ${compactionStatus}`, "info");
|
|
|
1851
1876
|
prepared,
|
|
1852
1877
|
model: structuredClone(model),
|
|
1853
1878
|
provider,
|
|
1854
|
-
options: { apiKey: options.apiKey, headers: structuredClone(options.headers), env: structuredClone(options.env), fetch: options.fetch },
|
|
1879
|
+
options: { apiKey: options.apiKey, headers: structuredClone(options.headers), env: structuredClone(options.env), fetch: options.fetch, ...typeof options.serviceTier === "string" ? { serviceTier: options.serviceTier } : {} },
|
|
1855
1880
|
branchLength: branch.length,
|
|
1856
1881
|
branchHash,
|
|
1857
1882
|
routeHash,
|
|
@@ -1905,6 +1930,7 @@ Last attempt: ${compactionStatus}`, "info");
|
|
|
1905
1930
|
removeInvalidation();
|
|
1906
1931
|
});
|
|
1907
1932
|
pi.on("session_before_compact", async (event, context) => {
|
|
1933
|
+
compactionStatus = "preparing compaction";
|
|
1908
1934
|
const capture = snapshot;
|
|
1909
1935
|
snapshot = undefined;
|
|
1910
1936
|
pending = undefined;
|
|
@@ -1938,7 +1964,7 @@ Last attempt: ${compactionStatus}`, "info");
|
|
|
1938
1964
|
const input = selectCoveredPrefix(capture.model, capture.prepared, prefix, fallback);
|
|
1939
1965
|
if (!input)
|
|
1940
1966
|
return;
|
|
1941
|
-
const current = () => ticket === generation && !event.signal.aborted && supportedRouteOptions(capture.model, capture.options) && !generic(context) && context.modelRegistry.getProvider(capture.model.provider) === capture.provider &&
|
|
1967
|
+
const current = () => ticket === generation && !event.signal.aborted && supportedRouteOptions(capture.model, capture.options) && !generic(context) && context.modelRegistry.getProvider(capture.model.provider) === capture.provider && unchanged(context.sessionManager.getBranch(), branch.length, branchHash);
|
|
1942
1968
|
const auth = await context.modelRegistry.getApiKeyAndHeaders(context.model);
|
|
1943
1969
|
if (!current())
|
|
1944
1970
|
return { cancel: true };
|
|
@@ -1951,7 +1977,10 @@ Last attempt: ${compactionStatus}`, "info");
|
|
|
1951
1977
|
try {
|
|
1952
1978
|
portable = await compact(event.preparation, capture.model, auth.apiKey, auth.headers, event.customInstructions, event.signal, context.thinkingLevel, capture.provider.streamSimple.bind(capture.provider), auth.env, undefined, undefined, capture.sessionId);
|
|
1953
1979
|
} catch {
|
|
1954
|
-
|
|
1980
|
+
if (!current())
|
|
1981
|
+
return { cancel: true };
|
|
1982
|
+
fallback("portable summary generation failed");
|
|
1983
|
+
return;
|
|
1955
1984
|
}
|
|
1956
1985
|
if (!current() || !portable.summary.trim() || portable.summary === NATIVE_COMPACTION_SUMMARY)
|
|
1957
1986
|
return { cancel: true };
|
|
@@ -1975,7 +2004,8 @@ Last attempt: ${compactionStatus}`, "info");
|
|
|
1975
2004
|
headers: capture.options.headers,
|
|
1976
2005
|
env: capture.options.env,
|
|
1977
2006
|
signal: event.signal,
|
|
1978
|
-
fetch: capture.options.fetch
|
|
2007
|
+
fetch: capture.options.fetch,
|
|
2008
|
+
...capture.options.serviceTier === undefined ? {} : { serviceTier: capture.options.serviceTier }
|
|
1979
2009
|
});
|
|
1980
2010
|
if (!current())
|
|
1981
2011
|
return { cancel: true };
|
|
@@ -2002,11 +2032,13 @@ Last attempt: ${compactionStatus}`, "info");
|
|
|
2002
2032
|
};
|
|
2003
2033
|
pending = { details, capture, summary: portable.summary };
|
|
2004
2034
|
return { compaction: { ...portable, usage: combineUsage(native.usage, portable.usage), details } };
|
|
2005
|
-
} catch {
|
|
2035
|
+
} catch (error) {
|
|
2006
2036
|
if (!current())
|
|
2007
2037
|
return { cancel: true };
|
|
2008
2038
|
fallback("native acquisition or validation failed");
|
|
2009
|
-
return {
|
|
2039
|
+
return {
|
|
2040
|
+
compaction: error instanceof NativeCompactionValidationError && error.usage !== undefined ? { ...portable, usage: combineUsage(error.usage, portable.usage) } : portable
|
|
2041
|
+
};
|
|
2010
2042
|
}
|
|
2011
2043
|
});
|
|
2012
2044
|
pi.on("session_compact", (event, context) => {
|
|
@@ -2024,9 +2056,18 @@ Last attempt: ${compactionStatus}`, "info");
|
|
|
2024
2056
|
return;
|
|
2025
2057
|
const branch = context.sessionManager.getBranch();
|
|
2026
2058
|
const coverage = candidate.details.coverage;
|
|
2027
|
-
|
|
2059
|
+
const index = branch.findIndex((entry) => entry.id === event.compactionEntry.id);
|
|
2060
|
+
if (!coverage || index < 0 || event.compactionEntry.firstKeptEntryId !== coverage.firstKeptEntryId || event.compactionEntry.parentId !== coverage.sourceLeafId || digest(branch.slice(0, index)) !== coverage.sourceHash)
|
|
2028
2061
|
return;
|
|
2029
|
-
active = {
|
|
2062
|
+
active = {
|
|
2063
|
+
entryId: event.compactionEntry.id,
|
|
2064
|
+
summary: candidate.summary,
|
|
2065
|
+
branchLength: index + 1,
|
|
2066
|
+
branchHash: digest(branch.slice(0, index + 1)),
|
|
2067
|
+
routeHash: candidate.capture.routeHash,
|
|
2068
|
+
policyHash: candidate.capture.policyHash,
|
|
2069
|
+
...typeof candidate.capture.prepared.payload.model === "string" ? { payloadModel: candidate.capture.prepared.payload.model } : {}
|
|
2070
|
+
};
|
|
2030
2071
|
});
|
|
2031
2072
|
pi.on("session_compact_failed", () => {
|
|
2032
2073
|
pending = undefined;
|
package/index.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { compact, convertToLlm, type ExtensionAPI, type ExtensionContext, type S
|
|
|
5
5
|
import { resolveCompactionModelPolicy } from "./policy";
|
|
6
6
|
import { installObserver, type InstalledObserver, type ObserveRequest } from "./provider";
|
|
7
7
|
import { capturePreparedRequest, equal, requestPolicy, selectCoveredPrefix, substituteSummary, summarySlot, type PreparedRequest } from "./replay";
|
|
8
|
-
import { isObject, requestProviderCompaction, resolveNativeProtocol, supportsResponsesApi, validateCompactedOutput, type NativeProtocol, type ResponseItem } from "./protocol";
|
|
8
|
+
import { isObject, NativeCompactionValidationError, requestProviderCompaction, resolveNativeProtocol, supportsResponsesApi, validateCompactedOutput, type NativeProtocol, type ResponseItem } from "./protocol";
|
|
9
9
|
|
|
10
10
|
export const NATIVE_COMPACTION_TYPE = "pi-provider-compaction/openai-responses";
|
|
11
11
|
export const NATIVE_COMPACTION_VERSION = 3;
|
|
@@ -124,7 +124,7 @@ interface Captured {
|
|
|
124
124
|
prepared: PreparedRequest;
|
|
125
125
|
model: Model<any>;
|
|
126
126
|
provider: Provider;
|
|
127
|
-
options: ProviderStreamOptions | SimpleStreamOptions;
|
|
127
|
+
options: (ProviderStreamOptions | SimpleStreamOptions) & { serviceTier?: string };
|
|
128
128
|
branchLength: number;
|
|
129
129
|
branchHash: string;
|
|
130
130
|
routeHash: string;
|
|
@@ -134,12 +134,13 @@ interface Captured {
|
|
|
134
134
|
}
|
|
135
135
|
interface Active {
|
|
136
136
|
entryId: string;
|
|
137
|
-
detailsHash: string;
|
|
138
137
|
summary: string;
|
|
139
138
|
branchLength: number;
|
|
140
139
|
branchHash: string;
|
|
141
140
|
routeHash: string;
|
|
142
141
|
policyHash: string;
|
|
142
|
+
/** Model field of the prepared payload; a request hook rewrite invalidates native state. */
|
|
143
|
+
payloadModel?: string;
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
export default function (pi: ExtensionAPI): void {
|
|
@@ -190,7 +191,12 @@ export default function (pi: ExtensionAPI): void {
|
|
|
190
191
|
}
|
|
191
192
|
if (options.sessionId !== manager.getSessionId()) return;
|
|
192
193
|
if (!supportsNativeCompaction(model)) { captureRejected("unsupported API"); return; }
|
|
193
|
-
if (!options.onPayload) {
|
|
194
|
+
if (!options.onPayload) {
|
|
195
|
+
// The portable-summary request re-enters this observer without a payload
|
|
196
|
+
// callback. Do not overwrite the main request's capture diagnostic.
|
|
197
|
+
if (captureStatus !== "eligible prepared request captured") captureRejected("no payload callback");
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
194
200
|
if (generic(owner)) { captureRejected("generic compaction model takes precedence"); return; }
|
|
195
201
|
// The Azure adapter also accepts API-specific overrides and process-env
|
|
196
202
|
// fallbacks. Those must not silently select a route absent from our evidence.
|
|
@@ -201,7 +207,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
201
207
|
const canonical = transcript(manager.buildSessionProjection().messages);
|
|
202
208
|
const current = () => {
|
|
203
209
|
if (generation !== ticket || ctx !== owner || options.signal?.aborted || !supportedRouteOptions(model, options)) return false;
|
|
204
|
-
try { return
|
|
210
|
+
try { return unchanged(owner.sessionManager.getBranch(), branch.length, branchHash); }
|
|
205
211
|
catch { retire(); return false; }
|
|
206
212
|
};
|
|
207
213
|
return async (payload) => {
|
|
@@ -225,8 +231,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
225
231
|
const routeHash = route(model, options);
|
|
226
232
|
const policyHash = digest(requestPolicy(model, prepared));
|
|
227
233
|
const latest = latestCompaction(branch);
|
|
234
|
+
const payloadModel = isObject(payload) && typeof payload.model === "string" ? payload.model : undefined;
|
|
228
235
|
let result = payload;
|
|
229
|
-
if (active && latest?.id === active.entryId && latest.summary === active.summary &&
|
|
236
|
+
if (active && latest?.id === active.entryId && latest.summary === active.summary && active.payloadModel === payloadModel && unchanged(branch, active.branchLength, active.branchHash) && active.routeHash === routeHash && active.policyHash === policyHash) {
|
|
230
237
|
const details = readNativeCompactionDetails(latest.details);
|
|
231
238
|
const projection = owner.sessionManager.buildSessionProjection();
|
|
232
239
|
const contribution = projection.entries.find((entry) => entry.sourceEntry.id === latest.id);
|
|
@@ -244,7 +251,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
244
251
|
captureStatus = "eligible prepared request captured";
|
|
245
252
|
snapshot = {
|
|
246
253
|
prepared, model: structuredClone(model), provider,
|
|
247
|
-
options: { apiKey: options.apiKey, headers: structuredClone(options.headers), env: structuredClone(options.env), fetch: options.fetch },
|
|
254
|
+
options: { apiKey: options.apiKey, headers: structuredClone(options.headers), env: structuredClone(options.env), fetch: options.fetch, ...(typeof (options as { serviceTier?: unknown }).serviceTier === "string" ? { serviceTier: (options as { serviceTier: string }).serviceTier } : {}) },
|
|
248
255
|
branchLength: branch.length, branchHash, routeHash, authHash: routeDigest(auth), policyHash,
|
|
249
256
|
sessionId: owner.sessionManager.getSessionId(),
|
|
250
257
|
};
|
|
@@ -281,6 +288,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
281
288
|
pi.on("session_shutdown", () => { if (typeof removeInvalidation === "function") removeInvalidation(); });
|
|
282
289
|
|
|
283
290
|
pi.on("session_before_compact", async (event, context) => {
|
|
291
|
+
compactionStatus = "preparing compaction";
|
|
284
292
|
const capture = snapshot;
|
|
285
293
|
snapshot = undefined; pending = undefined;
|
|
286
294
|
if (!capture) { fallback(`no eligible snapshot (${captureStatus})`); return; }
|
|
@@ -297,7 +305,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
297
305
|
const prefix = transcript(projection.entries.slice(0, boundary).flatMap((entry) => entry.messages));
|
|
298
306
|
const input = selectCoveredPrefix(capture.model, capture.prepared, prefix, fallback);
|
|
299
307
|
if (!input) return;
|
|
300
|
-
const current = () => ticket === generation && !event.signal.aborted && supportedRouteOptions(capture.model, capture.options) && !generic(context) && context.modelRegistry.getProvider(capture.model.provider) === capture.provider &&
|
|
308
|
+
const current = () => ticket === generation && !event.signal.aborted && supportedRouteOptions(capture.model, capture.options) && !generic(context) && context.modelRegistry.getProvider(capture.model.provider) === capture.provider && unchanged(context.sessionManager.getBranch(), branch.length, branchHash);
|
|
301
309
|
const auth = await context.modelRegistry.getApiKeyAndHeaders(context.model);
|
|
302
310
|
if (!current()) return { cancel: true };
|
|
303
311
|
if (!auth.ok || routeDigest(auth) !== capture.authHash) { fallback("auth changed since capture"); return; }
|
|
@@ -307,7 +315,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
307
315
|
portable = await compact(event.preparation, capture.model, auth.apiKey, auth.headers as Record<string, string> | undefined, event.customInstructions, event.signal, context.thinkingLevel, capture.provider.streamSimple.bind(capture.provider), auth.env, undefined, undefined, capture.sessionId);
|
|
308
316
|
} catch {
|
|
309
317
|
// No result exists to return; let Pi own its ordinary failure/fallback behavior.
|
|
310
|
-
|
|
318
|
+
if (!current()) return { cancel: true };
|
|
319
|
+
fallback("portable summary generation failed");
|
|
320
|
+
return undefined;
|
|
311
321
|
}
|
|
312
322
|
if (!current() || !portable.summary.trim() || portable.summary === NATIVE_COMPACTION_SUMMARY) return { cancel: true };
|
|
313
323
|
try {
|
|
@@ -320,6 +330,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
320
330
|
input, preparedPayload: capture.prepared.payload, protocol,
|
|
321
331
|
apiKey: capture.options.apiKey, headers: capture.options.headers, env: capture.options.env,
|
|
322
332
|
signal: event.signal, fetch: capture.options.fetch,
|
|
333
|
+
...(capture.options.serviceTier === undefined ? {} : { serviceTier: capture.options.serviceTier }),
|
|
323
334
|
});
|
|
324
335
|
if (!current()) return { cancel: true };
|
|
325
336
|
const finalAuth = await context.modelRegistry.getApiKeyAndHeaders(context.model);
|
|
@@ -334,12 +345,18 @@ export default function (pi: ExtensionAPI): void {
|
|
|
334
345
|
};
|
|
335
346
|
pending = { details, capture, summary: portable.summary };
|
|
336
347
|
return { compaction: { ...portable, usage: combineUsage(native.usage, portable.usage), details } };
|
|
337
|
-
} catch {
|
|
348
|
+
} catch (error) {
|
|
338
349
|
if (!current()) return { cancel: true };
|
|
339
350
|
// Do not expose exception text: provider errors may contain request data.
|
|
340
351
|
fallback("native acquisition or validation failed");
|
|
341
|
-
// The portable request already succeeded. Never pay for a duplicate summary
|
|
342
|
-
|
|
352
|
+
// The portable request already succeeded. Never pay for a duplicate summary,
|
|
353
|
+
// and keep the native call's usage when the endpoint answered but produced
|
|
354
|
+
// an unusable window.
|
|
355
|
+
return {
|
|
356
|
+
compaction: error instanceof NativeCompactionValidationError && error.usage !== undefined
|
|
357
|
+
? { ...portable, usage: combineUsage(error.usage, portable.usage) }
|
|
358
|
+
: portable,
|
|
359
|
+
};
|
|
343
360
|
}
|
|
344
361
|
});
|
|
345
362
|
pi.on("session_compact", (event, context) => {
|
|
@@ -352,8 +369,17 @@ export default function (pi: ExtensionAPI): void {
|
|
|
352
369
|
if (!candidate || !equal(event.compactionEntry.details, candidate.details) || event.compactionEntry.summary !== candidate.summary) return;
|
|
353
370
|
const branch = context.sessionManager.getBranch();
|
|
354
371
|
const coverage = candidate.details.coverage;
|
|
355
|
-
|
|
356
|
-
|
|
372
|
+
const index = branch.findIndex((entry) => entry.id === event.compactionEntry.id);
|
|
373
|
+
if (!coverage || index < 0 || event.compactionEntry.firstKeptEntryId !== coverage.firstKeptEntryId || event.compactionEntry.parentId !== coverage.sourceLeafId || digest(branch.slice(0, index)) !== coverage.sourceHash) return;
|
|
374
|
+
active = {
|
|
375
|
+
entryId: event.compactionEntry.id,
|
|
376
|
+
summary: candidate.summary,
|
|
377
|
+
branchLength: index + 1,
|
|
378
|
+
branchHash: digest(branch.slice(0, index + 1)),
|
|
379
|
+
routeHash: candidate.capture.routeHash,
|
|
380
|
+
policyHash: candidate.capture.policyHash,
|
|
381
|
+
...(typeof candidate.capture.prepared.payload.model === "string" ? { payloadModel: candidate.capture.prepared.payload.model } : {}),
|
|
382
|
+
};
|
|
357
383
|
});
|
|
358
384
|
pi.on("session_compact_failed", () => { pending = undefined; snapshot = undefined; compactionStatus = "compaction failed or cancelled"; });
|
|
359
385
|
}
|
package/package.json
CHANGED
package/protocol.ts
CHANGED
|
@@ -225,10 +225,20 @@ interface TransportRequest {
|
|
|
225
225
|
apiKey?: string;
|
|
226
226
|
headers?: ProviderHeaders;
|
|
227
227
|
env?: Record<string, string>;
|
|
228
|
+
/** Cost-relevant request option captured with the original request. */
|
|
229
|
+
serviceTier?: string;
|
|
228
230
|
signal: AbortSignal;
|
|
229
231
|
fetch?: typeof globalThis.fetch;
|
|
230
232
|
}
|
|
231
233
|
|
|
234
|
+
/** The provider answered and was billed, but its window cannot be committed. */
|
|
235
|
+
export class NativeCompactionValidationError extends Error {
|
|
236
|
+
constructor(message: string, readonly usage?: Usage) {
|
|
237
|
+
super(message);
|
|
238
|
+
this.name = "NativeCompactionValidationError";
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
232
242
|
export interface TransportResponse {
|
|
233
243
|
output: ResponseItem[];
|
|
234
244
|
usage: Usage;
|
|
@@ -258,8 +268,12 @@ async function requestResponsesCompact(request: TransportRequest): Promise<Trans
|
|
|
258
268
|
});
|
|
259
269
|
if (!response.ok) return response;
|
|
260
270
|
const parsed = await readJsonBounded(response, request.signal);
|
|
261
|
-
|
|
262
|
-
|
|
271
|
+
try {
|
|
272
|
+
if (!isObject(parsed)) throw new Error("Responses Compact returned a non-object response");
|
|
273
|
+
compacted = { response: parsed, output: validateCompactedOutput(parsed) };
|
|
274
|
+
} catch (error) {
|
|
275
|
+
throw new NativeCompactionValidationError(error instanceof Error ? error.message : String(error));
|
|
276
|
+
}
|
|
263
277
|
return syntheticCompletion(parsed, preparedPayload);
|
|
264
278
|
} catch (error) {
|
|
265
279
|
bridgeError = error;
|
|
@@ -272,6 +286,7 @@ async function requestResponsesCompact(request: TransportRequest): Promise<Trans
|
|
|
272
286
|
headers: request.headers,
|
|
273
287
|
env: request.env,
|
|
274
288
|
signal: request.signal,
|
|
289
|
+
...(request.serviceTier === undefined ? {} : { serviceTier: request.serviceTier }),
|
|
275
290
|
transport: "sse",
|
|
276
291
|
cacheRetention: "none",
|
|
277
292
|
maxRetries: 0,
|
|
@@ -288,6 +303,7 @@ async function requestResponsesCompact(request: TransportRequest): Promise<Trans
|
|
|
288
303
|
if (bridgeError) throw bridgeError;
|
|
289
304
|
throw error;
|
|
290
305
|
});
|
|
306
|
+
if (bridgeError instanceof NativeCompactionValidationError) throw new NativeCompactionValidationError(bridgeError.message, usage);
|
|
291
307
|
if (bridgeError) throw bridgeError;
|
|
292
308
|
if (!compacted || dispatches !== 1) throw new Error("Responses Compact did not complete exactly one request");
|
|
293
309
|
return { output: compacted.output, usage };
|
|
@@ -410,6 +426,7 @@ async function requestRemoteV2(request: TransportRequest): Promise<TransportResp
|
|
|
410
426
|
headers: request.headers,
|
|
411
427
|
env: request.env,
|
|
412
428
|
signal: request.signal,
|
|
429
|
+
...(request.serviceTier === undefined ? {} : { serviceTier: request.serviceTier }),
|
|
413
430
|
transport: "sse",
|
|
414
431
|
cacheRetention: "none",
|
|
415
432
|
maxRetries: 0,
|
|
@@ -436,5 +453,9 @@ export async function requestProviderCompaction(
|
|
|
436
453
|
request: TransportRequest & { protocol: NativeProtocol },
|
|
437
454
|
): Promise<TransportResponse> {
|
|
438
455
|
const result = await (request.protocol === "remote-v2" ? requestRemoteV2(request) : requestResponsesCompact(request));
|
|
439
|
-
|
|
456
|
+
try {
|
|
457
|
+
return { ...result, output: validateCompactedOutput(result) };
|
|
458
|
+
} catch (error) {
|
|
459
|
+
throw new NativeCompactionValidationError(error instanceof Error ? error.message : String(error), result.usage);
|
|
460
|
+
}
|
|
440
461
|
}
|
package/provider.ts
CHANGED
|
@@ -59,6 +59,11 @@ export function installObserver(pi: ExtensionAPI, ctx: ExtensionContext, observe
|
|
|
59
59
|
const id = model.provider;
|
|
60
60
|
const native = registry.getRegisteredNativeProvider(id);
|
|
61
61
|
if (native) {
|
|
62
|
+
// Pi composes a native base only when its catalog serves the active API.
|
|
63
|
+
// Otherwise the request goes to the global API provider and bypasses this
|
|
64
|
+
// wrapper; claiming an observer there would report native compaction as
|
|
65
|
+
// installed while never capturing anything.
|
|
66
|
+
if (typeof native.getModels !== "function" || !native.getModels().some((entry) => entry.api === model.api)) return;
|
|
62
67
|
const isInstalled = () => registry.getRegisteredNativeProvider(id) === wrapper;
|
|
63
68
|
const guarded: ObserveRequest = (...args) => {
|
|
64
69
|
if (!isInstalled()) return;
|