pi-openai-toolkit 0.17.1 → 0.17.3

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.1",
3
+ "version": "0.17.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -1,6 +1,5 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { ASTRA_MODEL_ID } from "../types";
3
- import { CODEX_CLIENT_VERSION } from "../responses-headers";
4
3
  import {
5
4
  getEffortControlState,
6
5
  planStableEffort,
@@ -101,20 +100,6 @@ export function registerCodexAstraExtension(pi: ExtensionAPI): void {
101
100
  return undefined;
102
101
  }
103
102
  });
104
-
105
- pi.on("before_provider_headers", (event, ctx) => {
106
- try {
107
- const model = ctx.model;
108
- if (!model || model.api !== "openai-codex-responses") return;
109
- // Live requests need the same backend version gate as our synthetic
110
- // ones: an old or missing `version` never reaches gated SKUs.
111
- if (!event.headers.version) {
112
- event.headers.version = CODEX_CLIENT_VERSION;
113
- }
114
- } catch {
115
- // Header injection is best-effort.
116
- }
117
- });
118
103
  }
119
104
 
120
105
  export default function codexAstraExtension(pi: ExtensionAPI): void {
@@ -146,7 +146,6 @@ export function codexContextProviderHeaders(
146
146
  headers.set("ChatGPT-Account-ID", provider.accountId);
147
147
  headers.set("originator", CODEX_TOOL_ORIGINATOR);
148
148
  headers.set("User-Agent", userAgent(CODEX_TOOL_VERSION));
149
- headers.set("version", CODEX_TOOL_VERSION);
150
149
  } else {
151
150
  headers.set("Authorization", `Bearer ${provider.apiKey}`);
152
151
  headers.delete("ChatGPT-Account-ID");
@@ -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
@@ -90,11 +90,9 @@ export function buildCodexCliUserAgent(version: string): string {
90
90
  }
91
91
 
92
92
  /**
93
- * The Codex backend version-gates model availability on the `version` header
94
- * (the pinned @openai/codex client version). `gpt-6-astra` requires >= 0.153.0;
95
- * an older or absent pin makes the SKU invisible to discovery and rejects
96
- * requests that name it. Keep this in sync with the newest gate Oh My Pi pins
97
- * in @oh-my-pi/pi-wire/codex (`CODEX_CLIENT_VERSION`).
93
+ * Codex client identity used by synthetic gateway requests in their User-Agent.
94
+ * Native live and context-management requests do not generate a `version`
95
+ * header; explicitly configured provider values remain caller-owned.
98
96
  */
99
97
  export const CODEX_CLIENT_VERSION = "0.153.0";
100
98
 
@@ -148,8 +146,6 @@ export function buildResponsesRequestHeaders(
148
146
  headers.set("originator", "pi");
149
147
  headers.set("user-agent", buildCodexUserAgent());
150
148
  headers.set("openai-beta", "responses=experimental");
151
- // Preserve the existing native Codex compatibility pin.
152
- headers.set("version", CODEX_CLIENT_VERSION);
153
149
  }
154
150
  // Gateways own their version policy. Explicit versions still pass through
155
151
  // the header merge and allowlist, but Toolkit does not generate one.