@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/provider.ts CHANGED
@@ -1,14 +1,27 @@
1
- import type { AssistantMessage, AssistantMessageEventStream, CacheRetention, Provider, StreamOptions, Model, TranscriptContext } from "@earendil-works/pi-ai";
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 stream = dispatch({
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
- // result() observes completion without consuming the public event iterator.
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 active = (token: symbol) => !closed && leases.get(token)?.current() === true && owned();
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-v1");
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(observe);
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(observe);
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 leaseEffectiveProvider(registry.getProvider(id), () => registry.getProvider(id), observe);
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(observe);
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(observe);
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
+ }