@nijaru/pi-provider-compaction 0.0.9 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -5
- package/dist/index.js +226 -123
- package/index.ts +63 -63
- package/package.json +3 -2
- package/policy.ts +9 -16
- package/protocol.ts +16 -13
- package/provider.ts +81 -14
- package/usage-observer.ts +52 -0
package/provider.ts
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { createAssistantMessageEventStream, type AssistantMessage, type AssistantMessageEventStream, type CacheRetention, type Provider, type StreamOptions, type Model, type TranscriptContext } from "@earendil-works/pi-ai";
|
|
2
2
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
interface UsageScope { sessionId?: string; }
|
|
4
|
+
const USAGE_OWNERS = Symbol.for("@nijaru/pi-provider-compaction/usage-owners");
|
|
5
|
+
const usageGlobal = globalThis as typeof globalThis & { [USAGE_OWNERS]?: WeakMap<object, string> };
|
|
6
|
+
const usageOwners = usageGlobal[USAGE_OWNERS] ??= new WeakMap<object, string>();
|
|
7
|
+
|
|
8
|
+
/** Associate the SDK-owned header record without reading or changing any headers. */
|
|
9
|
+
export function bindUsageHeaders(headers: object, sessionId: string): void {
|
|
10
|
+
usageOwners.set(headers, sessionId);
|
|
11
|
+
}
|
|
3
12
|
|
|
4
13
|
export type PayloadObserver = ((payload: unknown) => unknown | Promise<unknown>) & {
|
|
5
14
|
complete?: (message: AssistantMessage) => void;
|
|
15
|
+
/** Adjust only usage before any completion observer or consumer sees it. */
|
|
16
|
+
priceUsage?: (message: AssistantMessage) => void;
|
|
6
17
|
cacheRetention?: CacheRetention;
|
|
7
18
|
};
|
|
8
|
-
export type ObserveRequest = (model: Model<any>, context: TranscriptContext, options: StreamOptions) => PayloadObserver | undefined;
|
|
19
|
+
export type ObserveRequest = (model: Model<any>, context: TranscriptContext, options: StreamOptions, scope: UsageScope) => PayloadObserver | undefined;
|
|
9
20
|
|
|
10
21
|
export interface InstalledObserver {
|
|
11
22
|
provider: string;
|
|
23
|
+
/** installObserver leases one dispatch API; lower-level leases can be unscoped. */
|
|
24
|
+
api?: string;
|
|
12
25
|
isInstalled(): boolean;
|
|
13
26
|
remove(): void;
|
|
14
27
|
}
|
|
@@ -18,11 +31,13 @@ export interface InstalledObserver {
|
|
|
18
31
|
export const STREAM_OBSERVER = Symbol.for("@nijaru/pi-provider-compaction/stream-observer");
|
|
19
32
|
|
|
20
33
|
function observedStream<T extends StreamOptions>(model: Model<any>, context: TranscriptContext, options: T | undefined, observe: ObserveRequest, dispatch: (options: T | undefined) => AssistantMessageEventStream): AssistantMessageEventStream {
|
|
34
|
+
const headerOwner = options?.headers ? usageOwners.get(options.headers) : undefined;
|
|
35
|
+
const scope: UsageScope = { sessionId: headerOwner ?? options?.sessionId };
|
|
21
36
|
let handler: ReturnType<ObserveRequest>;
|
|
22
|
-
try { handler = observe(model, context, options ?? {}); }
|
|
37
|
+
try { handler = observe(model, context, options ?? {}, scope); }
|
|
23
38
|
catch { return dispatch(options); } // An expired optional observer must not veto dispatch.
|
|
24
39
|
if (!handler) return dispatch(options);
|
|
25
|
-
const
|
|
40
|
+
const run = () => dispatch({
|
|
26
41
|
...options,
|
|
27
42
|
...(handler.cacheRetention === undefined ? {} : { cacheRetention: handler.cacheRetention }),
|
|
28
43
|
onPayload: async (payload: unknown, requestModel: Model<any>) => {
|
|
@@ -32,16 +47,52 @@ function observedStream<T extends StreamOptions>(model: Model<any>, context: Tra
|
|
|
32
47
|
try { return await handler(prepared); } catch { return prepared; }
|
|
33
48
|
},
|
|
34
49
|
} as T);
|
|
35
|
-
|
|
50
|
+
const stream = run();
|
|
51
|
+
if (handler.priceUsage) return pricedStream(stream, model, handler);
|
|
52
|
+
// Pure capture remains transparent, including the original stream identity.
|
|
36
53
|
if (handler.complete) void stream.result().then(handler.complete).catch(() => {});
|
|
37
54
|
return stream;
|
|
38
55
|
}
|
|
39
56
|
|
|
57
|
+
/** Pricing must precede both terminal-event delivery and result() resolution. */
|
|
58
|
+
function pricedStream(stream: AssistantMessageEventStream, model: Model<any>, handler: PayloadObserver): AssistantMessageEventStream {
|
|
59
|
+
const out = createAssistantMessageEventStream();
|
|
60
|
+
void (async () => {
|
|
61
|
+
let final: AssistantMessage | undefined;
|
|
62
|
+
let partial: AssistantMessage | undefined;
|
|
63
|
+
try {
|
|
64
|
+
for await (const event of stream) {
|
|
65
|
+
if ("partial" in event) partial = event.partial;
|
|
66
|
+
if (event.type === "done" || event.type === "error") {
|
|
67
|
+
final = event.type === "done" ? event.message : event.error;
|
|
68
|
+
try { handler.priceUsage?.(final); } catch { /* Optional estimates never veto completion. */ }
|
|
69
|
+
try { handler.complete?.(final); } catch { /* Capture failure never vetoes completion. */ }
|
|
70
|
+
}
|
|
71
|
+
out.push(event);
|
|
72
|
+
}
|
|
73
|
+
if (final) { out.end(final); return; }
|
|
74
|
+
} catch { /* Malformed plugin streams still need a terminal outcome. */ }
|
|
75
|
+
const message: AssistantMessage = {
|
|
76
|
+
...(partial ?? {
|
|
77
|
+
role: "assistant", api: model.api, provider: model.provider, model: model.id,
|
|
78
|
+
content: [], timestamp: Date.now(),
|
|
79
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
|
|
80
|
+
}),
|
|
81
|
+
stopReason: "error", errorMessage: "Provider stream ended before a terminal event",
|
|
82
|
+
};
|
|
83
|
+
try { handler.priceUsage?.(message); } catch { /* Preserve the known usage on pricing failure. */ }
|
|
84
|
+
out.push({ type: "error", reason: "error", error: message });
|
|
85
|
+
out.end(message);
|
|
86
|
+
})();
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
40
90
|
/** One dispatch layer, with session-local selection and no released lease history. */
|
|
41
91
|
function observerLeases(provider: string, owned: () => boolean, restore: () => void) {
|
|
42
92
|
const leases = new Map<symbol, { observe: ObserveRequest; current: () => boolean }>();
|
|
43
93
|
let closed = false;
|
|
44
|
-
const
|
|
94
|
+
const live = (token: symbol) => !closed && leases.has(token);
|
|
95
|
+
const active = (token: symbol) => live(token) && leases.get(token)?.current() === true && owned();
|
|
45
96
|
const release = () => {
|
|
46
97
|
if (closed) return;
|
|
47
98
|
closed = true;
|
|
@@ -57,6 +108,10 @@ function observerLeases(provider: string, owned: () => boolean, restore: () => v
|
|
|
57
108
|
if (!handler) continue;
|
|
58
109
|
handlers.push(Object.assign((payload: unknown) => active(token) ? handler(payload) : payload, {
|
|
59
110
|
cacheRetention: handler.cacheRetention,
|
|
111
|
+
// A catalog/API recomposition may detach a provider after dispatch.
|
|
112
|
+
// Its in-flight bill still uses the sampled policy; explicit lease
|
|
113
|
+
// removal/disposal, unlike recomposition, revokes that callback.
|
|
114
|
+
priceUsage: handler.priceUsage ? (message: AssistantMessage) => { if (live(token)) handler.priceUsage!(message); } : undefined,
|
|
60
115
|
complete: handler.complete ? (message: AssistantMessage) => { if (active(token)) handler.complete!(message); } : undefined,
|
|
61
116
|
}));
|
|
62
117
|
} catch { /* A failed optional observer must not disable the other sessions. */ }
|
|
@@ -72,6 +127,11 @@ function observerLeases(provider: string, owned: () => boolean, restore: () => v
|
|
|
72
127
|
return payload;
|
|
73
128
|
}, {
|
|
74
129
|
cacheRetention: [...handlers].reverse().find((handler) => handler.cacheRetention !== undefined)?.cacheRetention,
|
|
130
|
+
priceUsage: handlers.some((handler) => handler.priceUsage) ? (message: AssistantMessage) => {
|
|
131
|
+
for (const handler of handlers) {
|
|
132
|
+
try { handler.priceUsage?.(message); } catch { /* Isolate independent pricing owners. */ }
|
|
133
|
+
}
|
|
134
|
+
} : undefined,
|
|
75
135
|
complete: handlers.some((handler) => handler.complete) ? (message: AssistantMessage) => {
|
|
76
136
|
for (const handler of handlers) {
|
|
77
137
|
try { handler.complete?.(message); } catch { /* Isolate optional completion observers too. */ }
|
|
@@ -95,7 +155,7 @@ function observerLeases(provider: string, owned: () => boolean, restore: () => v
|
|
|
95
155
|
type ObserverLeases = ReturnType<typeof observerLeases>;
|
|
96
156
|
// Pi's uncached module loader can instantiate this extension separately for
|
|
97
157
|
// sessions sharing one model runtime. Weak ownership must span those instances.
|
|
98
|
-
const LEASE_TABLES = Symbol.for("@nijaru/pi-provider-compaction/observer-leases-
|
|
158
|
+
const LEASE_TABLES = Symbol.for("@nijaru/pi-provider-compaction/observer-leases-v2");
|
|
99
159
|
interface LeaseTables {
|
|
100
160
|
effectiveLeases: WeakMap<Provider, { owned(): boolean; leases: ObserverLeases }>;
|
|
101
161
|
effectiveRestores: WeakMap<Function, { base: Provider; restore(): void }>;
|
|
@@ -168,20 +228,27 @@ export function leaseEffectiveProvider(base: Provider | undefined, current: () =
|
|
|
168
228
|
* delegate their original callback (including /fast). Never register an effective
|
|
169
229
|
* composed provider as a native base: that loses model headers and freezes config.
|
|
170
230
|
*/
|
|
171
|
-
export function installObserver(pi: ExtensionAPI, ctx: ExtensionContext, observe: ObserveRequest): InstalledObserver | undefined {
|
|
231
|
+
export function installObserver(pi: ExtensionAPI, ctx: Pick<ExtensionContext, "model" | "modelRegistry">, observe: ObserveRequest): InstalledObserver | undefined {
|
|
172
232
|
const model = ctx.model;
|
|
173
233
|
if (!model) return;
|
|
174
234
|
const registry = ctx.modelRegistry;
|
|
175
235
|
const id = model.provider;
|
|
236
|
+
const observeApi: ObserveRequest = (requestModel, ...args) => requestModel.api === model.api ? observe(requestModel, ...args) : undefined;
|
|
237
|
+
const tagged = (lease: InstalledObserver | undefined) => lease ? Object.assign(lease, { api: model.api }) : undefined;
|
|
238
|
+
const effective = () => {
|
|
239
|
+
const base = registry.getProvider(id);
|
|
240
|
+
if (!base?.getModels().some((entry) => entry.api === model.api)) return;
|
|
241
|
+
return tagged(leaseEffectiveProvider(base, () => registry.getProvider(id), observeApi));
|
|
242
|
+
};
|
|
176
243
|
const native = registry.getRegisteredNativeProvider(id);
|
|
177
244
|
if (native) {
|
|
178
245
|
// Pi composes a native base only when its catalog serves the active API.
|
|
179
246
|
// Otherwise the request goes to the global API provider and bypasses this
|
|
180
247
|
// wrapper; claiming an observer there would report native compaction as
|
|
181
248
|
// installed while never capturing anything.
|
|
182
|
-
if (typeof native.getModels !== "function" || !native.getModels().some((entry) => entry.api === model.api)) return;
|
|
249
|
+
if (typeof native.getModels !== "function" || !native.getModels().some((entry) => entry.api === model.api)) return effective();
|
|
183
250
|
const existing = nativeLeases.get(native);
|
|
184
|
-
if (existing) return existing.add(
|
|
251
|
+
if (existing) return tagged(existing.add(observeApi));
|
|
185
252
|
const leases = observerLeases(id, () => registry.getRegisteredNativeProvider(id) === wrapper, () => {
|
|
186
253
|
// Keep cleanup usable after bare SDK disposal invalidates the extension API.
|
|
187
254
|
if (registry.getRegisteredNativeProvider(id) === wrapper) registry.registerProvider(native);
|
|
@@ -190,7 +257,7 @@ export function installObserver(pi: ExtensionAPI, ctx: ExtensionContext, observe
|
|
|
190
257
|
const wrapper = decorateProvider(native, leases.observe);
|
|
191
258
|
nativeLeases.set(wrapper, leases);
|
|
192
259
|
pi.registerProvider(wrapper);
|
|
193
|
-
return leases.add(
|
|
260
|
+
return tagged(leases.add(observeApi));
|
|
194
261
|
}
|
|
195
262
|
const original = registry.getRegisteredProviderConfig(id);
|
|
196
263
|
// Refresh callbacks own mutable catalog state inside the composer. Replacing
|
|
@@ -198,12 +265,12 @@ export function installObserver(pi: ExtensionAPI, ctx: ExtensionContext, observe
|
|
|
198
265
|
if (!original?.streamSimple || original.refreshModels || original.oauth?.modifyModels || original.api !== model.api) {
|
|
199
266
|
// Lease the effective object's methods, NOT its registry slot. Re-registering
|
|
200
267
|
// a composed provider freezes catalogs and loses overlay/header semantics.
|
|
201
|
-
return
|
|
268
|
+
return effective();
|
|
202
269
|
}
|
|
203
270
|
// An injected legacy streamSimple overlay would also intercept full stream()
|
|
204
271
|
// calls and change reasoning semantics. Only decorate an existing stream owner.
|
|
205
272
|
const existing = legacyLeases.get(original.streamSimple);
|
|
206
|
-
if (existing) return existing.add(
|
|
273
|
+
if (existing) return tagged(existing.add(observeApi));
|
|
207
274
|
const delegate = original.streamSimple.bind(original);
|
|
208
275
|
const owned = () => registry.getRegisteredProviderConfig(id)?.streamSimple === wrapper && !registry.getRegisteredNativeProvider(id);
|
|
209
276
|
const leases = observerLeases(id, owned, () => {
|
|
@@ -219,5 +286,5 @@ export function installObserver(pi: ExtensionAPI, ctx: ExtensionContext, observe
|
|
|
219
286
|
Object.defineProperty(wrapper, STREAM_OBSERVER, { value: { delegate: original.streamSimple, release: leases.release } });
|
|
220
287
|
legacyLeases.set(wrapper, leases);
|
|
221
288
|
pi.registerProvider(id, { ...original, streamSimple: wrapper });
|
|
222
|
-
return leases.add(
|
|
289
|
+
return tagged(leases.add(observeApi));
|
|
223
290
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Usage } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { installObserver, type InstalledObserver } from "./provider";
|
|
4
|
+
|
|
5
|
+
const REQUEST = "pi-provider-compaction:observe-usage";
|
|
6
|
+
const READY = "pi-provider-compaction:usage-observer-ready";
|
|
7
|
+
type ModelKey = { provider: string; id: string };
|
|
8
|
+
type PriceUsage = (timestamp: number, usage: Usage) => void;
|
|
9
|
+
interface UsageRequest {
|
|
10
|
+
context: ExtensionContext;
|
|
11
|
+
model: NonNullable<ExtensionContext["model"]>;
|
|
12
|
+
priceFor(model: ModelKey): PriceUsage;
|
|
13
|
+
respond(observer: InstalledObserver | undefined): void;
|
|
14
|
+
handled?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Share the existing stream owner, rather than stacking competing pricing wrappers. */
|
|
18
|
+
export function serveUsageObservers(pi: ExtensionAPI, current: () => ExtensionContext | undefined) {
|
|
19
|
+
const leases = new Set<InstalledObserver>();
|
|
20
|
+
const remove = pi.events.on(REQUEST, (value) => {
|
|
21
|
+
const request = value as Partial<UsageRequest> | undefined;
|
|
22
|
+
const owner = current();
|
|
23
|
+
if (!owner || !request || request.handled || !request.context || !request.model || typeof request.priceFor !== "function" || typeof request.respond !== "function") return;
|
|
24
|
+
try {
|
|
25
|
+
if (request.context.modelRegistry !== owner.modelRegistry || request.context.sessionManager.getSessionId() !== owner.sessionManager.getSessionId()) return;
|
|
26
|
+
request.handled = true;
|
|
27
|
+
const sessionId = owner.sessionManager.getSessionId();
|
|
28
|
+
const installed = installObserver(pi, { model: request.model, modelRegistry: owner.modelRegistry }, (model, _context, _options, scope) => {
|
|
29
|
+
// The SDK header event identifies the owner even when a branch
|
|
30
|
+
// summary has an independent routing id. Sample policy at dispatch.
|
|
31
|
+
const price = request.priceFor!({ provider: model.provider, id: model.id });
|
|
32
|
+
return Object.assign((payload: unknown) => payload, { priceUsage: (message: { timestamp: number; usage: Usage }) => {
|
|
33
|
+
if (scope.sessionId === sessionId) price(message.timestamp, message.usage);
|
|
34
|
+
} });
|
|
35
|
+
});
|
|
36
|
+
if (!installed) { request.respond(undefined); return; }
|
|
37
|
+
const lease: InstalledObserver = {
|
|
38
|
+
provider: installed.provider,
|
|
39
|
+
api: installed.api,
|
|
40
|
+
isInstalled: () => installed.isInstalled(),
|
|
41
|
+
remove() { leases.delete(lease); installed.remove(); },
|
|
42
|
+
};
|
|
43
|
+
leases.add(lease);
|
|
44
|
+
request.respond(lease);
|
|
45
|
+
} catch { /* Optional cost estimates must not veto a provider request. */ }
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
ready(context: ExtensionContext) { pi.events.emit(READY, { context }); },
|
|
49
|
+
clear() { for (const lease of leases) lease.remove(); },
|
|
50
|
+
dispose() { remove(); for (const lease of leases) lease.remove(); },
|
|
51
|
+
};
|
|
52
|
+
}
|