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
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
-
*
|
|
7
|
-
*
|
|
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,
|
|
79
|
-
const result =
|
|
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