pi-openai-toolkit 0.17.0 → 0.17.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-openai-toolkit",
3
- "version": "0.17.0",
3
+ "version": "0.17.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -153,7 +153,8 @@ export function codexContextProviderHeaders(
153
153
  headers.delete("Cookie");
154
154
  headers.set("originator", CODEX_GATEWAY_ORIGINATOR);
155
155
  headers.set("User-Agent", userAgent(CODEX_CLIENT_VERSION));
156
- headers.set("version", CODEX_CLIENT_VERSION);
156
+ // Let the gateway own its version policy; preserve an explicitly configured
157
+ // version already copied from provider.headers above.
157
158
  headers.set("X-Codex-Affinity-Scope", CODEX_AFFINITY_SCOPE);
158
159
  headers.set("X-Codex-Model", provider.model);
159
160
  }
@@ -1,12 +1,14 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import type { AgentMessage } from "@earendil-works/pi-agent-core";
3
3
  import type { ProviderHeaders } from "@earendil-works/pi-ai";
4
- import type {
5
- CompactionResult,
6
- ExtensionAPI,
7
- ExtensionContext,
8
- SessionBeforeCompactEvent,
9
- SessionEntry,
4
+ import {
5
+ calculateContextTokens,
6
+ estimateTokens,
7
+ type CompactionResult,
8
+ type ExtensionAPI,
9
+ type ExtensionContext,
10
+ type SessionBeforeCompactEvent,
11
+ type SessionEntry,
10
12
  } from "@earendil-works/pi-coding-agent";
11
13
  import { ContextWindowBudget, type ContextRemaining } from "./window-budget";
12
14
  import type { WindowBulkReport } from "./window-bulk";
@@ -86,6 +88,12 @@ export class CodexContextWindowManager {
86
88
  private lastKnownSystemHead: AgentMessage | undefined;
87
89
  private bulkReport: WindowBulkReport | undefined;
88
90
  private bulkSurfacedKey: string | undefined;
91
+ /**
92
+ * Pi's getContextUsage() measures the durable transcript. Remote windows are
93
+ * projected only by this extension, so remember the last provider-visible
94
+ * estimate for budget decisions and get_context_remaining.
95
+ */
96
+ private projectedContextTokens: number | undefined;
89
97
  private readonly loadThreadHint: ThreadHintLoader;
90
98
 
91
99
  constructor(loadThreadHint?: ThreadHintLoader) {
@@ -149,6 +157,7 @@ export class CodexContextWindowManager {
149
157
  this.branchStateInvalidated = false;
150
158
  this.budget.reset();
151
159
  this.trimPendingWindowId = undefined;
160
+ this.projectedContextTokens = undefined;
152
161
  }
153
162
 
154
163
  restore(entries: readonly SessionEntry[], sessionId?: string): void {
@@ -240,6 +249,7 @@ export class CodexContextWindowManager {
240
249
  mode: "off" | "remote",
241
250
  ): AgentMessage[] {
242
251
  if (mode === "off") {
252
+ this.projectedContextTokens = undefined;
243
253
  return messages.filter(
244
254
  (message) => message.role !== "custom" || message.customType !== CODEX_CONTEXT_WINDOW_MESSAGE_TYPE,
245
255
  );
@@ -266,11 +276,16 @@ export class CodexContextWindowManager {
266
276
  this.restoredMarkerId = undefined;
267
277
  this.budget.reset();
268
278
  this.trimPendingWindowId = undefined;
279
+ this.projectedContextTokens = undefined;
269
280
  }
270
281
  // Projection observes the request view, never durable session state. A
271
282
  // queued rollover marker must not clear the duplicate guard here.
272
283
  const trimmed = boundaryIndex < 0 ? [...messages] : this.trimToWindow(messages, boundaryIndex);
273
- return mode === "remote" ? projectEncryptedToolResults(trimmed) : trimmed;
284
+ const projected = mode === "remote" ? projectEncryptedToolResults(trimmed) : trimmed;
285
+ this.projectedContextTokens = mode === "remote" && this.identity
286
+ ? estimateProjectedWindowTokens(projected)
287
+ : undefined;
288
+ return projected;
274
289
  }
275
290
 
276
291
  /**
@@ -363,7 +378,12 @@ export class CodexContextWindowManager {
363
378
  // once-per-window reminder on a false alarm seconds after a successful
364
379
  // rollover, leaving the window silent for the rest of its life.
365
380
  if (!hasAssistantUsageSinceWindowBoundary(ctx.sessionManager.getBranch(), this.sessionId)) return;
366
- const reminder = this.budget.record(ctx, this.identity, contextTokens, contextReminderThresholdPercent);
381
+ const reminder = this.budget.record(
382
+ ctx,
383
+ this.identity,
384
+ contextTokens ?? this.projectedContextTokens,
385
+ contextReminderThresholdPercent,
386
+ );
367
387
  if (!reminder) return;
368
388
  sendContextWindowMessage(
369
389
  pi,
@@ -375,7 +395,7 @@ export class CodexContextWindowManager {
375
395
  }
376
396
 
377
397
  remaining(ctx: ExtensionContext, contextTokens?: number): ContextRemaining {
378
- return this.budget.remaining(ctx, this.identity, contextTokens);
398
+ return this.budget.remaining(ctx, this.identity, contextTokens ?? this.projectedContextTokens);
379
399
  }
380
400
 
381
401
  /**
@@ -703,6 +723,35 @@ function matchesSession(markerSessionId: string | undefined, sessionId: string |
703
723
  return sessionId === undefined || markerSessionId === sessionId;
704
724
  }
705
725
 
726
+ function estimateProjectedWindowTokens(messages: readonly AgentMessage[]): number {
727
+ let lastUsageIndex = -1;
728
+ let usageTokens = 0;
729
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
730
+ const message = messages[index]!;
731
+ if (message.role !== "assistant" || message.stopReason === "aborted" || message.stopReason === "error" || !message.usage) continue;
732
+ const tokens = calculateContextTokens(message.usage);
733
+ if (tokens <= 0) continue;
734
+ lastUsageIndex = index;
735
+ usageTokens = tokens;
736
+ break;
737
+ }
738
+ if (lastUsageIndex < 0) return messages.reduce((total, message) => total + estimateProjectedMessageTokens(message), 0);
739
+ return usageTokens + messages
740
+ .slice(lastUsageIndex + 1)
741
+ .reduce((total, message) => total + estimateProjectedMessageTokens(message), 0);
742
+ }
743
+
744
+ function estimateProjectedMessageTokens(message: AgentMessage): number {
745
+ // A synthetic custom marker may be represented without content in a test or
746
+ // by an older host. It contributes no measurable text; it must not make the
747
+ // context hook fail closed merely because the diagnostic estimate is missing.
748
+ if (
749
+ !("content" in message) ||
750
+ (typeof message.content !== "string" && !Array.isArray(message.content))
751
+ ) return 0;
752
+ return estimateTokens(message);
753
+ }
754
+
706
755
  function projectEncryptedToolResults(messages: readonly AgentMessage[]): AgentMessage[] {
707
756
  let changed = false;
708
757
  const projected = messages.map((message) => {
@@ -876,13 +876,16 @@ async function handleContextInternal(
876
876
  const remoteActive = await remoteContextActive(ctx, config);
877
877
  if (remoteActive) {
878
878
  try {
879
+ // Measure the provider-visible current window, not Pi's durable transcript.
880
+ // The latter still contains retired remote windows and can be inflated by
881
+ // an upstream error even though this request only carries the live window.
882
+ const projected = contextWindows.project(event.messages, "remote");
879
883
  contextWindows.recordBudget(
880
884
  pi,
881
885
  ctx,
882
886
  true,
883
887
  config.contextReminderThresholdPercent,
884
888
  );
885
- const projected = contextWindows.project(event.messages, "remote");
886
889
  reportProjectionDiagnostics(contextWindows, config, ctx);
887
890
  return projected.length === event.messages.length && projected.every((message, index) => message === event.messages[index])
888
891
  ? undefined
@@ -1378,11 +1381,12 @@ export default function registerCompactionExtension(
1378
1381
  for (const existing of Object.keys(event.headers)) {
1379
1382
  if (!allowedGatewayHeaders.has(existing.toLowerCase())) delete event.headers[existing];
1380
1383
  }
1384
+ // Keep any caller-supplied version without forcing a Toolkit pin.
1385
+ // The gateway allowlist above already permits it.
1381
1386
  for (const name of [
1382
1387
  "authorization",
1383
1388
  "originator",
1384
1389
  "user-agent",
1385
- "version",
1386
1390
  "session-id",
1387
1391
  "x-client-request-id",
1388
1392
  "x-codex-affinity-scope",
@@ -148,10 +148,11 @@ export function buildResponsesRequestHeaders(
148
148
  headers.set("originator", "pi");
149
149
  headers.set("user-agent", buildCodexUserAgent());
150
150
  headers.set("openai-beta", "responses=experimental");
151
+ // Preserve the existing native Codex compatibility pin.
152
+ headers.set("version", CODEX_CLIENT_VERSION);
151
153
  }
152
- // Codex backend gating: a request without a new-enough `version` never
153
- // reaches model SKUs that arrived behind the gate (gpt-6-astra).
154
- headers.set("version", CODEX_CLIENT_VERSION);
154
+ // Gateways own their version policy. Explicit versions still pass through
155
+ // the header merge and allowlist, but Toolkit does not generate one.
155
156
  // Prompt-cache/session affinity rides on the conversation identity. The
156
157
  // gateway path also uses the bare model hint above so NEWapi can route
157
158
  // context/compact requests without inspecting their body.