pi-ui-extend 1.0.15 → 1.0.16

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.
@@ -1,11 +1,13 @@
1
1
  /**
2
- * WORKAROUND for @earendil-works/pi-ai bug: the Codex / OpenAI Responses API
3
- * rejects HTTP 400 `Unknown parameter: 'input[N].content'` when non-message
4
- * items (reasoning, function_call_output) carry a spurious `content` field.
2
+ * WORKAROUNDS for Codex / OpenAI Responses payload compatibility:
3
+ * - non-message items (reasoning, function_call_output) must not carry the
4
+ * spurious `content` field rejected with HTTP 400;
5
+ * - the Codex backend rejects the legacy `prompt_cache_retention` field for
6
+ * all current models, while direct OpenAI GPT-5.6+ uses the newer prompt
7
+ * cache options shape.
5
8
  *
6
- * Stray fields can come from replayed pi-ai items or from another extension
7
- * that modifies the provider payload. The sanitizer therefore must be the
8
- * LAST `before_provider_request` handler registered by pi-tools-suite.
9
+ * The sanitizer must be the LAST `before_provider_request` handler registered
10
+ * by pi-tools-suite so another payload hook cannot undo these guards.
9
11
  *
10
12
  * In pi-ai >= 0.80.6 the final `before_provider_request` payload feeds both the
11
13
  * WebSocket delta builder and the zstd-compressed SSE fallback body. Running the
@@ -27,6 +29,9 @@ type ProviderRequestContext = {
27
29
  model?: unknown;
28
30
  };
29
31
 
32
+ const OPENAI_CODEX_PROVIDER = "openai-codex";
33
+ const OPENAI_PROVIDER = "openai";
34
+
30
35
  /**
31
36
  * Strip spurious `content` from any object that carries an `input` or
32
37
  * `messages` array. Shared core for payload sanitization.
@@ -75,12 +80,21 @@ export default function codexReasoningFix(pi: ExtensionAPI): void {
75
80
  // src/index.ts deliberately registers this module last. A later payload
76
81
  // modifier could otherwise reintroduce invalid content after sanitization,
77
82
  // and transport encoding happens after this hook.
78
- pi.on("before_provider_request", async (event: ProviderRequestEvent, _ctx: ProviderRequestContext) => {
79
- const result = stripReasoningContentFromPayload(event.payload);
83
+ pi.on("before_provider_request", async (event: ProviderRequestEvent, ctx: ProviderRequestContext) => {
84
+ const result = sanitizeCodexProviderPayload(event.payload, ctx.model);
80
85
  return result === event.payload ? undefined : result;
81
86
  });
82
87
  }
83
88
 
89
+ /**
90
+ * Apply all final Codex payload compatibility guards. Returns the original
91
+ * reference when no guard changes the payload.
92
+ */
93
+ export function sanitizeCodexProviderPayload(payload: unknown, model: unknown): unknown {
94
+ const contentSanitized = stripReasoningContentFromPayload(payload);
95
+ return stripUnsupportedPromptCacheRetention(contentSanitized, model);
96
+ }
97
+
84
98
  /**
85
99
  * Strip spurious `content` from non-message items in a full payload. Returns the same reference
86
100
  * when nothing changed; exported for unit testing.
@@ -90,6 +104,72 @@ export function stripReasoningContentFromPayload(payload: unknown): unknown {
90
104
  return result ? result.obj : payload;
91
105
  }
92
106
 
107
+ /**
108
+ * Remove legacy prompt-cache retention only where it is known to be rejected:
109
+ * every current Codex model, and direct OpenAI GPT-5.6 or newer. Match the
110
+ * selected model rather than a bare payload id so other providers keep the
111
+ * field untouched.
112
+ */
113
+ export function stripUnsupportedPromptCacheRetention(payload: unknown, model: unknown): unknown {
114
+ if (!isRecord(payload) || !Object.prototype.hasOwnProperty.call(payload, "prompt_cache_retention")) {
115
+ return payload;
116
+ }
117
+ if (!rejectsLegacyPromptCacheRetention(model, payload.model)) return payload;
118
+
119
+ const { prompt_cache_retention: _drop, ...rest } = payload;
120
+ return rest;
121
+ }
122
+
123
+ function rejectsLegacyPromptCacheRetention(model: unknown, payloadModel: unknown): boolean {
124
+ const selected = modelIdentity(model);
125
+ if (selected.provider !== undefined) {
126
+ return isAffectedProviderModel(selected.provider, selected.id);
127
+ }
128
+ if (selected.id?.includes("/")) {
129
+ return isAffectedQualifiedModel(selected.id);
130
+ }
131
+
132
+ return typeof payloadModel === "string" && isAffectedQualifiedModel(payloadModel.trim().toLowerCase());
133
+ }
134
+
135
+ function isAffectedQualifiedModel(modelRef: string): boolean {
136
+ const slash = modelRef.indexOf("/");
137
+ if (slash <= 0 || slash === modelRef.length - 1) return false;
138
+ return isAffectedProviderModel(modelRef.slice(0, slash), modelRef.slice(slash + 1));
139
+ }
140
+
141
+ function isAffectedProviderModel(provider: string, modelId: string | undefined): boolean {
142
+ if (provider === OPENAI_CODEX_PROVIDER) return true;
143
+ return provider === OPENAI_PROVIDER && modelId !== undefined && isGpt56OrNewer(modelId);
144
+ }
145
+
146
+ function isGpt56OrNewer(modelId: string): boolean {
147
+ const match = /^gpt-(\d+)(?:\.(\d+))?(?:-|$)/u.exec(modelId);
148
+ if (!match) return false;
149
+ const major = Number(match[1]);
150
+ const minor = Number(match[2] ?? 0);
151
+ return major > 5 || (major === 5 && minor >= 6);
152
+ }
153
+
154
+ function modelIdentity(model: unknown): { provider?: string; id?: string } {
155
+ if (typeof model === "string") return { id: model.trim().toLowerCase() };
156
+ if (!isRecord(model)) return {};
157
+
158
+ const provider = firstString(model.provider, model.providerId, model.providerID);
159
+ const id = firstString(model.id, model.modelId, model.modelID, model.model);
160
+ return {
161
+ provider: provider?.toLowerCase(),
162
+ id: id?.toLowerCase(),
163
+ };
164
+ }
165
+
166
+ function firstString(...values: unknown[]): string | undefined {
167
+ for (const value of values) {
168
+ if (typeof value === "string" && value.trim()) return value.trim();
169
+ }
170
+ return undefined;
171
+ }
172
+
93
173
  function isRecord(value: unknown): value is Record<string, unknown> {
94
174
  return value !== null && typeof value === "object" && !Array.isArray(value);
95
175
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-ui-extend",
3
- "version": "1.0.15",
3
+ "version": "1.0.16",
4
4
  "description": "Pix: a workspace-first terminal UI for Pi with tabs, readable tool activity, voice input, and bundled agent tools.",
5
5
  "private": false,
6
6
  "repository": {