@nijaru/pi-provider-compaction 0.0.2 → 0.0.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/dist/index.js +1583 -0
- package/package.json +8 -7
package/dist/index.js
ADDED
|
@@ -0,0 +1,1583 @@
|
|
|
1
|
+
// node_modules/@earendil-works/pi-ai/dist/utils/hash.js
|
|
2
|
+
function shortHash(str) {
|
|
3
|
+
let h1 = 3735928559;
|
|
4
|
+
let h2 = 1103547991;
|
|
5
|
+
for (let i = 0;i < str.length; i++) {
|
|
6
|
+
const ch = str.charCodeAt(i);
|
|
7
|
+
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
8
|
+
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
9
|
+
}
|
|
10
|
+
h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
|
|
11
|
+
h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
|
|
12
|
+
return (h2 >>> 0).toString(36) + (h1 >>> 0).toString(36);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// node_modules/@earendil-works/pi-ai/dist/utils/sanitize-unicode.js
|
|
16
|
+
function sanitizeSurrogates(text) {
|
|
17
|
+
return text.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// node_modules/@earendil-works/pi-ai/dist/api/constrained-sampling.js
|
|
21
|
+
class UnsupportedStrictJsonSchemaError extends Error {
|
|
22
|
+
}
|
|
23
|
+
var UNSUPPORTED_STRICT_SCHEMA_KEYS = [
|
|
24
|
+
"$ref",
|
|
25
|
+
"$defs",
|
|
26
|
+
"definitions",
|
|
27
|
+
"allOf",
|
|
28
|
+
"oneOf",
|
|
29
|
+
"patternProperties",
|
|
30
|
+
"dependentSchemas",
|
|
31
|
+
"dependencies",
|
|
32
|
+
"unevaluatedProperties",
|
|
33
|
+
"propertyNames",
|
|
34
|
+
"contains",
|
|
35
|
+
"prefixItems",
|
|
36
|
+
"not",
|
|
37
|
+
"if",
|
|
38
|
+
"then",
|
|
39
|
+
"else"
|
|
40
|
+
];
|
|
41
|
+
function isJsonSchemaObject(value) {
|
|
42
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
43
|
+
}
|
|
44
|
+
function isStructuredSchema(schema) {
|
|
45
|
+
if (!isJsonSchemaObject(schema))
|
|
46
|
+
return false;
|
|
47
|
+
const types = typeof schema.type === "string" ? [schema.type] : Array.isArray(schema.type) ? schema.type : [];
|
|
48
|
+
return types.includes("object") || types.includes("array") || schema.properties !== undefined || schema.items !== undefined;
|
|
49
|
+
}
|
|
50
|
+
function schemaAllowsNull(schema) {
|
|
51
|
+
if (!isJsonSchemaObject(schema))
|
|
52
|
+
return false;
|
|
53
|
+
if (schema.type === "null" || Array.isArray(schema.type) && schema.type.includes("null"))
|
|
54
|
+
return true;
|
|
55
|
+
if (schema.const === null || Array.isArray(schema.enum) && schema.enum.includes(null))
|
|
56
|
+
return true;
|
|
57
|
+
return Array.isArray(schema.anyOf) && schema.anyOf.some((variant) => schemaAllowsNull(variant));
|
|
58
|
+
}
|
|
59
|
+
function makeJsonSchemaNodeStrict(schema) {
|
|
60
|
+
if (!isJsonSchemaObject(schema)) {
|
|
61
|
+
throw new UnsupportedStrictJsonSchemaError("boolean schemas are unsupported");
|
|
62
|
+
}
|
|
63
|
+
for (const key of UNSUPPORTED_STRICT_SCHEMA_KEYS) {
|
|
64
|
+
if (schema[key] !== undefined) {
|
|
65
|
+
throw new UnsupportedStrictJsonSchemaError(`${key} schemas are unsupported`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (schema.anyOf !== undefined) {
|
|
69
|
+
if (!Array.isArray(schema.anyOf) || schema.anyOf.length === 0) {
|
|
70
|
+
throw new UnsupportedStrictJsonSchemaError("anyOf must contain at least one schema");
|
|
71
|
+
}
|
|
72
|
+
for (const variant of schema.anyOf) {
|
|
73
|
+
if (isStructuredSchema(variant)) {
|
|
74
|
+
throw new UnsupportedStrictJsonSchemaError("object and array unions are unsupported");
|
|
75
|
+
}
|
|
76
|
+
makeJsonSchemaNodeStrict(variant);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (schema.items !== undefined) {
|
|
80
|
+
if (Array.isArray(schema.items)) {
|
|
81
|
+
throw new UnsupportedStrictJsonSchemaError("tuple schemas are unsupported");
|
|
82
|
+
}
|
|
83
|
+
makeJsonSchemaNodeStrict(schema.items);
|
|
84
|
+
}
|
|
85
|
+
const isObjectSchema = schema.type === "object";
|
|
86
|
+
if (schema.properties !== undefined && !isObjectSchema) {
|
|
87
|
+
throw new UnsupportedStrictJsonSchemaError("properties require type object");
|
|
88
|
+
}
|
|
89
|
+
if (!isObjectSchema)
|
|
90
|
+
return;
|
|
91
|
+
if (schema.additionalProperties !== undefined && schema.additionalProperties !== false) {
|
|
92
|
+
throw new UnsupportedStrictJsonSchemaError("schema-valued or true additionalProperties is unsupported");
|
|
93
|
+
}
|
|
94
|
+
if (schema.properties !== undefined && !isJsonSchemaObject(schema.properties)) {
|
|
95
|
+
throw new UnsupportedStrictJsonSchemaError("object properties must be a schema map");
|
|
96
|
+
}
|
|
97
|
+
if (schema.required !== undefined && (!Array.isArray(schema.required) || schema.required.some((key) => typeof key !== "string"))) {
|
|
98
|
+
throw new UnsupportedStrictJsonSchemaError("object required must be a string array");
|
|
99
|
+
}
|
|
100
|
+
const properties = schema.properties ?? {};
|
|
101
|
+
const propertyNames = Object.keys(properties);
|
|
102
|
+
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
103
|
+
if ([...required].some((key) => !propertyNames.includes(key))) {
|
|
104
|
+
throw new UnsupportedStrictJsonSchemaError("required contains an unknown property");
|
|
105
|
+
}
|
|
106
|
+
for (const [key, property] of Object.entries(properties)) {
|
|
107
|
+
makeJsonSchemaNodeStrict(property);
|
|
108
|
+
if (!required.has(key) && !schemaAllowsNull(property)) {
|
|
109
|
+
properties[key] = { anyOf: [property, { type: "null" }] };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
schema.required = propertyNames;
|
|
113
|
+
schema.additionalProperties = false;
|
|
114
|
+
}
|
|
115
|
+
function makeStrictJsonSchema(schema) {
|
|
116
|
+
const cloned = structuredClone(schema);
|
|
117
|
+
if (!isJsonSchemaObject(cloned)) {
|
|
118
|
+
throw new UnsupportedStrictJsonSchemaError("root schema must have type object");
|
|
119
|
+
}
|
|
120
|
+
makeJsonSchemaNodeStrict(cloned);
|
|
121
|
+
if (cloned.type !== "object") {
|
|
122
|
+
throw new UnsupportedStrictJsonSchemaError("root schema must have type object");
|
|
123
|
+
}
|
|
124
|
+
return cloned;
|
|
125
|
+
}
|
|
126
|
+
function getJsonSchemaToolParameters(tool, strict) {
|
|
127
|
+
return strict === true ? makeStrictJsonSchema(tool.parameters) : tool.parameters;
|
|
128
|
+
}
|
|
129
|
+
function getGrammarToolInput(toolName, arguments_, inputProperty) {
|
|
130
|
+
const input = arguments_[inputProperty];
|
|
131
|
+
if (typeof input !== "string") {
|
|
132
|
+
throw new Error(`Grammar tool call "${toolName}" requires argument "${inputProperty}" to be a string.`);
|
|
133
|
+
}
|
|
134
|
+
return input;
|
|
135
|
+
}
|
|
136
|
+
function inferGrammarInputProperty(tool) {
|
|
137
|
+
const schema = tool.parameters;
|
|
138
|
+
if (schema.type !== "object") {
|
|
139
|
+
throw new Error("grammar constrained sampling requires an object parameter schema");
|
|
140
|
+
}
|
|
141
|
+
if (!Array.isArray(schema.required) || schema.required.length !== 1 || typeof schema.required[0] !== "string") {
|
|
142
|
+
throw new Error("grammar constrained sampling requires exactly one required string property");
|
|
143
|
+
}
|
|
144
|
+
const inputProperty = schema.required[0];
|
|
145
|
+
if (!schema.properties?.[inputProperty]) {
|
|
146
|
+
throw new Error(`grammar constrained sampling requires a properties entry for ${inputProperty}`);
|
|
147
|
+
}
|
|
148
|
+
if (schema.properties[inputProperty]?.type !== "string") {
|
|
149
|
+
throw new Error(`grammar constrained sampling property ${inputProperty} must have type string`);
|
|
150
|
+
}
|
|
151
|
+
return inputProperty;
|
|
152
|
+
}
|
|
153
|
+
function resolveJsonSchemaStrictSampling(tool, supportsStrictMode) {
|
|
154
|
+
const config = tool.constrainedSampling;
|
|
155
|
+
if (!config || config.type !== "json_schema")
|
|
156
|
+
return;
|
|
157
|
+
if (supportsStrictMode) {
|
|
158
|
+
try {
|
|
159
|
+
makeStrictJsonSchema(tool.parameters);
|
|
160
|
+
return true;
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (!(error instanceof UnsupportedStrictJsonSchemaError))
|
|
163
|
+
throw error;
|
|
164
|
+
if (config.strict !== "require")
|
|
165
|
+
return;
|
|
166
|
+
throw new Error(`Tool "${tool.name}" requires JSON-schema constrained sampling, but ${error.message}.`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (config.strict === "require") {
|
|
170
|
+
throw new Error(`Tool "${tool.name}" requires JSON-schema constrained sampling, but strict tools are unsupported.`);
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
function resolveGrammarConstrainedSampling(tool, supportsOpenAIGrammarTools) {
|
|
175
|
+
const config = tool.constrainedSampling;
|
|
176
|
+
if (!config || config.type !== "grammar") {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (!supportsOpenAIGrammarTools) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const larkDefinition = config.variants.openai_lark;
|
|
183
|
+
const regexDefinition = config.variants.openai_regex;
|
|
184
|
+
const hasLarkDefinition = typeof larkDefinition === "string" && larkDefinition.trim().length > 0;
|
|
185
|
+
const hasRegexDefinition = typeof regexDefinition === "string" && regexDefinition.trim().length > 0;
|
|
186
|
+
if (!hasLarkDefinition && !hasRegexDefinition) {
|
|
187
|
+
throw new Error(`Tool "${tool.name}" cannot use grammar constrained sampling: no supported grammar variant was provided.`);
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
return {
|
|
191
|
+
format: hasLarkDefinition ? "lark" : "regex",
|
|
192
|
+
definition: hasLarkDefinition ? larkDefinition : regexDefinition,
|
|
193
|
+
inputProperty: inferGrammarInputProperty(tool)
|
|
194
|
+
};
|
|
195
|
+
} catch (error) {
|
|
196
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
197
|
+
throw new Error(`Tool "${tool.name}" cannot use grammar constrained sampling: ${message}.`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// node_modules/@earendil-works/pi-ai/dist/api/transform-messages.js
|
|
202
|
+
var NON_VISION_USER_IMAGE_PLACEHOLDER = "(image omitted: model does not support images)";
|
|
203
|
+
var NON_VISION_TOOL_IMAGE_PLACEHOLDER = "(tool image omitted: model does not support images)";
|
|
204
|
+
function replaceImagesWithPlaceholder(content, placeholder) {
|
|
205
|
+
const result = [];
|
|
206
|
+
let previousWasPlaceholder = false;
|
|
207
|
+
for (const block of content) {
|
|
208
|
+
if (block.type === "image") {
|
|
209
|
+
if (!previousWasPlaceholder) {
|
|
210
|
+
result.push({ type: "text", text: placeholder });
|
|
211
|
+
}
|
|
212
|
+
previousWasPlaceholder = true;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
result.push(block);
|
|
216
|
+
previousWasPlaceholder = block.text === placeholder;
|
|
217
|
+
}
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
function downgradeUnsupportedImages(messages, model) {
|
|
221
|
+
if (model.input.includes("image")) {
|
|
222
|
+
return messages;
|
|
223
|
+
}
|
|
224
|
+
return messages.map((msg) => {
|
|
225
|
+
if (msg.role === "user" && Array.isArray(msg.content)) {
|
|
226
|
+
return {
|
|
227
|
+
...msg,
|
|
228
|
+
content: replaceImagesWithPlaceholder(msg.content, NON_VISION_USER_IMAGE_PLACEHOLDER)
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
if (msg.role === "toolResult") {
|
|
232
|
+
return {
|
|
233
|
+
...msg,
|
|
234
|
+
content: replaceImagesWithPlaceholder(msg.content, NON_VISION_TOOL_IMAGE_PLACEHOLDER)
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
return msg;
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
function transformMessages(messages, model, normalizeToolCallId) {
|
|
241
|
+
const toolCallIdMap = new Map;
|
|
242
|
+
const normalizedMessages = messages.map((msg) => msg.content == null ? { ...msg, content: [] } : msg);
|
|
243
|
+
const imageAwareMessages = downgradeUnsupportedImages(normalizedMessages, model);
|
|
244
|
+
const transformed = imageAwareMessages.map((msg) => {
|
|
245
|
+
if (msg.role === "user") {
|
|
246
|
+
return msg;
|
|
247
|
+
}
|
|
248
|
+
if (msg.role === "toolResult") {
|
|
249
|
+
const normalizedId = toolCallIdMap.get(msg.toolCallId);
|
|
250
|
+
if (normalizedId && normalizedId !== msg.toolCallId) {
|
|
251
|
+
return { ...msg, toolCallId: normalizedId };
|
|
252
|
+
}
|
|
253
|
+
return msg;
|
|
254
|
+
}
|
|
255
|
+
if (msg.role === "assistant") {
|
|
256
|
+
const assistantMsg = msg;
|
|
257
|
+
const isSameModel = assistantMsg.provider === model.provider && assistantMsg.api === model.api && assistantMsg.model === model.id;
|
|
258
|
+
const transformedContent = assistantMsg.content.flatMap((block) => {
|
|
259
|
+
if (block.type === "thinking") {
|
|
260
|
+
if (block.redacted) {
|
|
261
|
+
return isSameModel ? block : [];
|
|
262
|
+
}
|
|
263
|
+
if (isSameModel && block.thinkingSignature)
|
|
264
|
+
return block;
|
|
265
|
+
if (!block.thinking || block.thinking.trim() === "")
|
|
266
|
+
return [];
|
|
267
|
+
if (isSameModel)
|
|
268
|
+
return block;
|
|
269
|
+
return {
|
|
270
|
+
type: "text",
|
|
271
|
+
text: block.thinking
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
if (block.type === "text") {
|
|
275
|
+
if (isSameModel)
|
|
276
|
+
return block;
|
|
277
|
+
return {
|
|
278
|
+
type: "text",
|
|
279
|
+
text: block.text
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
if (block.type === "toolCall") {
|
|
283
|
+
const toolCall = block;
|
|
284
|
+
let normalizedToolCall = toolCall;
|
|
285
|
+
if (!isSameModel && toolCall.thoughtSignature) {
|
|
286
|
+
normalizedToolCall = { ...toolCall };
|
|
287
|
+
delete normalizedToolCall.thoughtSignature;
|
|
288
|
+
}
|
|
289
|
+
if (!isSameModel && normalizeToolCallId) {
|
|
290
|
+
const normalizedId = normalizeToolCallId(toolCall.id, model, assistantMsg);
|
|
291
|
+
if (normalizedId !== toolCall.id) {
|
|
292
|
+
toolCallIdMap.set(toolCall.id, normalizedId);
|
|
293
|
+
normalizedToolCall = { ...normalizedToolCall, id: normalizedId };
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return normalizedToolCall;
|
|
297
|
+
}
|
|
298
|
+
return block;
|
|
299
|
+
});
|
|
300
|
+
return {
|
|
301
|
+
...assistantMsg,
|
|
302
|
+
content: transformedContent
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
return msg;
|
|
306
|
+
});
|
|
307
|
+
const result = [];
|
|
308
|
+
let pendingToolCalls = [];
|
|
309
|
+
let existingToolResultIds = new Set;
|
|
310
|
+
const insertSyntheticToolResults = () => {
|
|
311
|
+
if (pendingToolCalls.length > 0) {
|
|
312
|
+
for (const tc of pendingToolCalls) {
|
|
313
|
+
if (!existingToolResultIds.has(tc.id)) {
|
|
314
|
+
result.push({
|
|
315
|
+
role: "toolResult",
|
|
316
|
+
toolCallId: tc.id,
|
|
317
|
+
toolName: tc.name,
|
|
318
|
+
content: [{ type: "text", text: "No result provided" }],
|
|
319
|
+
isError: true,
|
|
320
|
+
timestamp: Date.now()
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
pendingToolCalls = [];
|
|
325
|
+
existingToolResultIds = new Set;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
for (let i = 0;i < transformed.length; i++) {
|
|
329
|
+
const msg = transformed[i];
|
|
330
|
+
if (msg.role === "assistant") {
|
|
331
|
+
insertSyntheticToolResults();
|
|
332
|
+
const assistantMsg = msg;
|
|
333
|
+
if (assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted") {
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
const toolCalls = assistantMsg.content.filter((b) => b.type === "toolCall");
|
|
337
|
+
if (toolCalls.length > 0) {
|
|
338
|
+
pendingToolCalls = toolCalls;
|
|
339
|
+
existingToolResultIds = new Set;
|
|
340
|
+
}
|
|
341
|
+
result.push(msg);
|
|
342
|
+
} else if (msg.role === "toolResult") {
|
|
343
|
+
existingToolResultIds.add(msg.toolCallId);
|
|
344
|
+
result.push(msg);
|
|
345
|
+
} else if (msg.role === "user") {
|
|
346
|
+
insertSyntheticToolResults();
|
|
347
|
+
result.push(msg);
|
|
348
|
+
} else {
|
|
349
|
+
result.push(msg);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
insertSyntheticToolResults();
|
|
353
|
+
return result;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// node_modules/@earendil-works/pi-ai/dist/api/openai-responses-shared.js
|
|
357
|
+
function parseTextSignature(signature) {
|
|
358
|
+
if (!signature)
|
|
359
|
+
return;
|
|
360
|
+
if (signature.startsWith("{")) {
|
|
361
|
+
try {
|
|
362
|
+
const parsed = JSON.parse(signature);
|
|
363
|
+
if (parsed.v === 1 && typeof parsed.id === "string") {
|
|
364
|
+
if (parsed.phase === "commentary" || parsed.phase === "final_answer") {
|
|
365
|
+
return { id: parsed.id, phase: parsed.phase };
|
|
366
|
+
}
|
|
367
|
+
return { id: parsed.id };
|
|
368
|
+
}
|
|
369
|
+
} catch {}
|
|
370
|
+
}
|
|
371
|
+
return { id: signature };
|
|
372
|
+
}
|
|
373
|
+
function convertToolResultOutput(model, content) {
|
|
374
|
+
const textResult = content.filter((c) => c.type === "text").map((c) => c.text).join(`
|
|
375
|
+
`);
|
|
376
|
+
const images = content.filter((c) => c.type === "image");
|
|
377
|
+
const hasText = textResult.length > 0;
|
|
378
|
+
if (images.length === 0 || !model.input.includes("image")) {
|
|
379
|
+
return sanitizeSurrogates(hasText ? textResult : images.length > 0 ? "(see attached image)" : "(no tool output)");
|
|
380
|
+
}
|
|
381
|
+
const output = [];
|
|
382
|
+
if (hasText) {
|
|
383
|
+
output.push({ type: "input_text", text: sanitizeSurrogates(textResult) });
|
|
384
|
+
}
|
|
385
|
+
for (const image of images) {
|
|
386
|
+
output.push({
|
|
387
|
+
type: "input_image",
|
|
388
|
+
detail: "auto",
|
|
389
|
+
image_url: `data:${image.mimeType};base64,${image.data}`
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
return output;
|
|
393
|
+
}
|
|
394
|
+
function convertResponsesMessages(model, context, allowedToolCallProviders, options) {
|
|
395
|
+
const messages = [];
|
|
396
|
+
const loadedToolNames = new Set;
|
|
397
|
+
const normalizeIdPart = (part) => {
|
|
398
|
+
const sanitized = part.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
399
|
+
const normalized = sanitized.length > 64 ? sanitized.slice(0, 64) : sanitized;
|
|
400
|
+
return normalized.replace(/_+$/, "");
|
|
401
|
+
};
|
|
402
|
+
const buildForeignResponsesItemId = (itemId) => {
|
|
403
|
+
const normalized = `fc_${shortHash(itemId)}`;
|
|
404
|
+
return normalized.length > 64 ? normalized.slice(0, 64) : normalized;
|
|
405
|
+
};
|
|
406
|
+
const normalizeToolCallId = (id, _targetModel, source) => {
|
|
407
|
+
if (!allowedToolCallProviders.has(model.provider))
|
|
408
|
+
return normalizeIdPart(id);
|
|
409
|
+
if (!id.includes("|"))
|
|
410
|
+
return normalizeIdPart(id);
|
|
411
|
+
const [callId, itemId] = id.split("|");
|
|
412
|
+
const normalizedCallId = normalizeIdPart(callId);
|
|
413
|
+
const isForeignToolCall = source.provider !== model.provider || source.api !== model.api;
|
|
414
|
+
let normalizedItemId = isForeignToolCall ? buildForeignResponsesItemId(itemId) : normalizeIdPart(itemId);
|
|
415
|
+
if (!normalizedItemId.startsWith("fc_")) {
|
|
416
|
+
normalizedItemId = normalizeIdPart(`fc_${normalizedItemId}`);
|
|
417
|
+
}
|
|
418
|
+
return `${normalizedCallId}|${normalizedItemId}`;
|
|
419
|
+
};
|
|
420
|
+
const transformedMessages = transformMessages(context.messages, model, normalizeToolCallId);
|
|
421
|
+
const includeSystemPrompt = options?.includeSystemPrompt ?? true;
|
|
422
|
+
if (includeSystemPrompt && context.systemPrompt) {
|
|
423
|
+
const compat = model.compat;
|
|
424
|
+
const role = model.reasoning && compat?.supportsDeveloperRole !== false ? "developer" : "system";
|
|
425
|
+
messages.push({
|
|
426
|
+
role,
|
|
427
|
+
content: sanitizeSurrogates(context.systemPrompt)
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
let msgIndex = 0;
|
|
431
|
+
for (const msg of transformedMessages) {
|
|
432
|
+
if (msg.role === "user") {
|
|
433
|
+
if (typeof msg.content === "string") {
|
|
434
|
+
messages.push({
|
|
435
|
+
role: "user",
|
|
436
|
+
content: [{ type: "input_text", text: sanitizeSurrogates(msg.content) }]
|
|
437
|
+
});
|
|
438
|
+
} else {
|
|
439
|
+
const content = msg.content.map((item) => {
|
|
440
|
+
if (item.type === "text") {
|
|
441
|
+
return {
|
|
442
|
+
type: "input_text",
|
|
443
|
+
text: sanitizeSurrogates(item.text)
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
type: "input_image",
|
|
448
|
+
detail: "auto",
|
|
449
|
+
image_url: `data:${item.mimeType};base64,${item.data}`
|
|
450
|
+
};
|
|
451
|
+
});
|
|
452
|
+
if (content.length === 0)
|
|
453
|
+
continue;
|
|
454
|
+
messages.push({
|
|
455
|
+
role: "user",
|
|
456
|
+
content
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
} else if (msg.role === "assistant") {
|
|
460
|
+
const output = [];
|
|
461
|
+
const assistantMsg = msg;
|
|
462
|
+
const isSameProviderAndApi = assistantMsg.provider === model.provider && assistantMsg.api === model.api;
|
|
463
|
+
const isSameModel = isSameProviderAndApi && assistantMsg.model === model.id;
|
|
464
|
+
const isDifferentModel = isSameProviderAndApi && assistantMsg.model !== model.id;
|
|
465
|
+
let textBlockIndex = 0;
|
|
466
|
+
for (const block of msg.content) {
|
|
467
|
+
if (block.type === "thinking") {
|
|
468
|
+
if (block.thinkingSignature) {
|
|
469
|
+
const reasoningItem = JSON.parse(block.thinkingSignature);
|
|
470
|
+
output.push(reasoningItem);
|
|
471
|
+
}
|
|
472
|
+
} else if (block.type === "text") {
|
|
473
|
+
const textBlock = block;
|
|
474
|
+
const parsedSignature = parseTextSignature(textBlock.textSignature);
|
|
475
|
+
const fallbackMessageId = textBlockIndex === 0 ? `msg_pi_${msgIndex}` : `msg_pi_${msgIndex}_${textBlockIndex}`;
|
|
476
|
+
textBlockIndex++;
|
|
477
|
+
let msgId = parsedSignature?.id;
|
|
478
|
+
if (!msgId) {
|
|
479
|
+
msgId = fallbackMessageId;
|
|
480
|
+
} else if (msgId.length > 64) {
|
|
481
|
+
msgId = `msg_${shortHash(msgId)}`;
|
|
482
|
+
}
|
|
483
|
+
output.push({
|
|
484
|
+
type: "message",
|
|
485
|
+
role: "assistant",
|
|
486
|
+
content: [{ type: "output_text", text: sanitizeSurrogates(textBlock.text), annotations: [] }],
|
|
487
|
+
status: "completed",
|
|
488
|
+
id: msgId,
|
|
489
|
+
phase: parsedSignature?.phase
|
|
490
|
+
});
|
|
491
|
+
} else if (block.type === "toolCall") {
|
|
492
|
+
const toolCall = block;
|
|
493
|
+
const [callId, itemIdRaw] = toolCall.id.split("|");
|
|
494
|
+
const customInputProperty = options?.grammarToolInputProperties?.get(toolCall.name);
|
|
495
|
+
let itemId = itemIdRaw;
|
|
496
|
+
if (isDifferentModel && itemId?.startsWith("fc_") || customInputProperty === undefined && !itemId?.startsWith("fc_")) {
|
|
497
|
+
itemId = undefined;
|
|
498
|
+
}
|
|
499
|
+
const canReplayNamespace = isSameModel || options?.deferredTools?.has(toolCall.name) === true;
|
|
500
|
+
if (customInputProperty !== undefined) {
|
|
501
|
+
output.push({
|
|
502
|
+
type: "custom_tool_call",
|
|
503
|
+
id: itemId,
|
|
504
|
+
call_id: callId,
|
|
505
|
+
name: toolCall.name,
|
|
506
|
+
input: sanitizeSurrogates(getGrammarToolInput(toolCall.name, toolCall.arguments, customInputProperty)),
|
|
507
|
+
...canReplayNamespace && toolCall.namespace !== undefined ? { namespace: toolCall.namespace } : {}
|
|
508
|
+
});
|
|
509
|
+
} else {
|
|
510
|
+
output.push({
|
|
511
|
+
type: "function_call",
|
|
512
|
+
id: itemId,
|
|
513
|
+
call_id: callId,
|
|
514
|
+
name: toolCall.name,
|
|
515
|
+
arguments: JSON.stringify(toolCall.arguments),
|
|
516
|
+
...canReplayNamespace && toolCall.namespace !== undefined ? { namespace: toolCall.namespace } : {}
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
if (output.length === 0)
|
|
522
|
+
continue;
|
|
523
|
+
messages.push(...output);
|
|
524
|
+
} else if (msg.role === "toolResult") {
|
|
525
|
+
const [callId] = msg.toolCallId.split("|");
|
|
526
|
+
const output = convertToolResultOutput(model, msg.content);
|
|
527
|
+
if (options?.grammarToolInputProperties?.has(msg.toolName)) {
|
|
528
|
+
messages.push({
|
|
529
|
+
type: "custom_tool_call_output",
|
|
530
|
+
call_id: callId,
|
|
531
|
+
output
|
|
532
|
+
});
|
|
533
|
+
} else {
|
|
534
|
+
messages.push({
|
|
535
|
+
type: "function_call_output",
|
|
536
|
+
call_id: callId,
|
|
537
|
+
output
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
const deferredTools = [];
|
|
541
|
+
for (const name of msg.addedToolNames ?? []) {
|
|
542
|
+
const tool = options?.deferredTools?.get(name);
|
|
543
|
+
if (!tool || loadedToolNames.has(name))
|
|
544
|
+
continue;
|
|
545
|
+
loadedToolNames.add(name);
|
|
546
|
+
deferredTools.push(tool);
|
|
547
|
+
}
|
|
548
|
+
if (deferredTools.length > 0 && options?.deferredToolsMode === "additional-tools") {
|
|
549
|
+
messages.push({
|
|
550
|
+
type: "additional_tools",
|
|
551
|
+
role: "developer",
|
|
552
|
+
tools: convertResponsesTools(deferredTools, options.toolOptions)
|
|
553
|
+
});
|
|
554
|
+
} else if (deferredTools.length > 0 && options?.deferredToolsMode === "tool-search") {
|
|
555
|
+
const names = deferredTools.map((tool) => tool.name);
|
|
556
|
+
const searchCallId = `pi_tool_load_${shortHash(`${msg.toolCallId}:${names.join(",")}`)}`;
|
|
557
|
+
messages.push({
|
|
558
|
+
type: "tool_search_call",
|
|
559
|
+
call_id: searchCallId,
|
|
560
|
+
execution: "client",
|
|
561
|
+
status: "completed",
|
|
562
|
+
arguments: { query: names.join(" "), limit: names.length }
|
|
563
|
+
});
|
|
564
|
+
messages.push({
|
|
565
|
+
type: "tool_search_output",
|
|
566
|
+
call_id: searchCallId,
|
|
567
|
+
execution: "client",
|
|
568
|
+
status: "completed",
|
|
569
|
+
tools: convertResponsesTools(deferredTools, {
|
|
570
|
+
...options.toolOptions,
|
|
571
|
+
deferLoading: true
|
|
572
|
+
})
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
msgIndex++;
|
|
577
|
+
}
|
|
578
|
+
return messages;
|
|
579
|
+
}
|
|
580
|
+
function convertResponsesTools(tools, options) {
|
|
581
|
+
const defaultStrict = options?.strict === undefined ? false : options.strict;
|
|
582
|
+
const supportsStrictMode = options?.supportsStrictMode ?? true;
|
|
583
|
+
const supportsOpenAIGrammarTools = options?.supportsOpenAIGrammarTools ?? false;
|
|
584
|
+
return tools.map((tool) => {
|
|
585
|
+
const grammar = resolveGrammarConstrainedSampling(tool, supportsOpenAIGrammarTools);
|
|
586
|
+
if (grammar) {
|
|
587
|
+
return {
|
|
588
|
+
type: "custom",
|
|
589
|
+
name: tool.name,
|
|
590
|
+
description: tool.description,
|
|
591
|
+
format: {
|
|
592
|
+
type: "grammar",
|
|
593
|
+
syntax: grammar.format,
|
|
594
|
+
definition: grammar.definition
|
|
595
|
+
},
|
|
596
|
+
...options?.deferLoading ? { defer_loading: true } : {}
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
const constrainedStrict = resolveJsonSchemaStrictSampling(tool, supportsStrictMode);
|
|
600
|
+
const strict = constrainedStrict ?? defaultStrict;
|
|
601
|
+
const functionTool = {
|
|
602
|
+
type: "function",
|
|
603
|
+
name: tool.name,
|
|
604
|
+
description: tool.description,
|
|
605
|
+
parameters: getJsonSchemaToolParameters(tool, strict === true),
|
|
606
|
+
...options?.deferLoading ? { defer_loading: true } : {}
|
|
607
|
+
};
|
|
608
|
+
if (supportsStrictMode) {
|
|
609
|
+
functionTool.strict = strict;
|
|
610
|
+
}
|
|
611
|
+
return functionTool;
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// index.ts
|
|
616
|
+
import { calculateCost } from "@earendil-works/pi-ai";
|
|
617
|
+
import {
|
|
618
|
+
compact,
|
|
619
|
+
convertToLlm,
|
|
620
|
+
sessionEntryToContextMessages
|
|
621
|
+
} from "@earendil-works/pi-coding-agent";
|
|
622
|
+
|
|
623
|
+
// policy.ts
|
|
624
|
+
import {
|
|
625
|
+
CONFIG_DIR_NAME,
|
|
626
|
+
getAgentDir,
|
|
627
|
+
hasTrustRequiringProjectResources,
|
|
628
|
+
ProjectTrustStore
|
|
629
|
+
} from "@earendil-works/pi-coding-agent";
|
|
630
|
+
import { closeSync, constants as fsConstants, fstatSync, openSync, readSync } from "node:fs";
|
|
631
|
+
import { join, resolve, sep } from "node:path";
|
|
632
|
+
var COMPACTION_MODEL_FLAG = "compaction-model";
|
|
633
|
+
var PROJECT_POLICY = "compaction-policy.json";
|
|
634
|
+
var AGENT_DIR_OVERRIDE = "PI_COMPACTOR_AGENT_DIR";
|
|
635
|
+
var MAX_POLICY_BYTES = 64 * 1024;
|
|
636
|
+
var MAX_MODEL_SELECTORS = 8;
|
|
637
|
+
var MAX_SELECTOR_LENGTH = 512;
|
|
638
|
+
function normalizeSelector(value) {
|
|
639
|
+
if (typeof value !== "string")
|
|
640
|
+
return;
|
|
641
|
+
const selector = value.trim();
|
|
642
|
+
if (selector.length === 0 || selector.length > MAX_SELECTOR_LENGTH)
|
|
643
|
+
return;
|
|
644
|
+
return selector;
|
|
645
|
+
}
|
|
646
|
+
function parsePolicySelectors(value) {
|
|
647
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
648
|
+
return;
|
|
649
|
+
if (!("models" in value) || !Array.isArray(value.models))
|
|
650
|
+
return;
|
|
651
|
+
const selectors = [];
|
|
652
|
+
for (const model of value.models) {
|
|
653
|
+
const selector = normalizeSelector(model);
|
|
654
|
+
if (selector === undefined || selectors.includes(selector))
|
|
655
|
+
continue;
|
|
656
|
+
selectors.push(selector);
|
|
657
|
+
if (selectors.length === MAX_MODEL_SELECTORS)
|
|
658
|
+
break;
|
|
659
|
+
}
|
|
660
|
+
return selectors;
|
|
661
|
+
}
|
|
662
|
+
function readPolicySelectors(configPath) {
|
|
663
|
+
let fd;
|
|
664
|
+
try {
|
|
665
|
+
fd = openSync(configPath, fsConstants.O_RDONLY | fsConstants.O_NONBLOCK);
|
|
666
|
+
const stat = fstatSync(fd);
|
|
667
|
+
if (!stat.isFile() || stat.size > MAX_POLICY_BYTES)
|
|
668
|
+
return;
|
|
669
|
+
const buffer = Buffer.allocUnsafe(MAX_POLICY_BYTES + 1);
|
|
670
|
+
let bytesRead = 0;
|
|
671
|
+
while (bytesRead < buffer.length) {
|
|
672
|
+
const count = readSync(fd, buffer, bytesRead, buffer.length - bytesRead, null);
|
|
673
|
+
if (count === 0)
|
|
674
|
+
break;
|
|
675
|
+
bytesRead += count;
|
|
676
|
+
}
|
|
677
|
+
if (bytesRead > MAX_POLICY_BYTES)
|
|
678
|
+
return;
|
|
679
|
+
return parsePolicySelectors(JSON.parse(buffer.subarray(0, bytesRead).toString("utf8")));
|
|
680
|
+
} catch {
|
|
681
|
+
return;
|
|
682
|
+
} finally {
|
|
683
|
+
if (fd !== undefined) {
|
|
684
|
+
try {
|
|
685
|
+
closeSync(fd);
|
|
686
|
+
} catch {}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
function activeAgentDir(ctx) {
|
|
691
|
+
const override = process.env[AGENT_DIR_OVERRIDE]?.trim();
|
|
692
|
+
if (override)
|
|
693
|
+
return resolve(override);
|
|
694
|
+
try {
|
|
695
|
+
const sessionDir = ctx.sessionManager?.getSessionDir();
|
|
696
|
+
if (sessionDir) {
|
|
697
|
+
const resolvedCwd = resolve(ctx.cwd);
|
|
698
|
+
const safePath = `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
|
699
|
+
const suffix = `${sep}${join("sessions", safePath)}`;
|
|
700
|
+
const resolvedSessionDir = resolve(sessionDir);
|
|
701
|
+
if (resolvedSessionDir.endsWith(suffix)) {
|
|
702
|
+
return resolvedSessionDir.slice(0, -suffix.length);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
} catch {}
|
|
706
|
+
return getAgentDir();
|
|
707
|
+
}
|
|
708
|
+
function isProjectPolicyTrusted(ctx, agentDir) {
|
|
709
|
+
try {
|
|
710
|
+
if (!ctx.isProjectTrusted())
|
|
711
|
+
return false;
|
|
712
|
+
return hasTrustRequiringProjectResources(ctx.cwd) || new ProjectTrustStore(agentDir).get(ctx.cwd) === true;
|
|
713
|
+
} catch {
|
|
714
|
+
return false;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
function readCompactionModelFlagFromArgv(argv = process.argv.slice(2)) {
|
|
718
|
+
let value;
|
|
719
|
+
const prefix = `--${COMPACTION_MODEL_FLAG}=`;
|
|
720
|
+
for (let i = 0;i < argv.length; i++) {
|
|
721
|
+
const arg = argv[i];
|
|
722
|
+
if (arg === "--")
|
|
723
|
+
break;
|
|
724
|
+
if (arg === `--${COMPACTION_MODEL_FLAG}`) {
|
|
725
|
+
const next = argv[i + 1];
|
|
726
|
+
if (next !== undefined && !next.startsWith("-") && !next.startsWith("@")) {
|
|
727
|
+
value = next;
|
|
728
|
+
i++;
|
|
729
|
+
} else {
|
|
730
|
+
value = true;
|
|
731
|
+
}
|
|
732
|
+
} else if (arg.startsWith(prefix)) {
|
|
733
|
+
value = arg.slice(prefix.length);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
return value;
|
|
737
|
+
}
|
|
738
|
+
function resolveCompactionModelPolicy(pi, ctx, argv = process.argv.slice(2)) {
|
|
739
|
+
const argvRaw = readCompactionModelFlagFromArgv(argv);
|
|
740
|
+
if (typeof argvRaw === "string" && argvRaw.trim()) {
|
|
741
|
+
const selector = normalizeSelector(argvRaw);
|
|
742
|
+
return selector ? { hasSelectors: true, source: "flag" } : { hasSelectors: false };
|
|
743
|
+
}
|
|
744
|
+
try {
|
|
745
|
+
const rawFlag = pi.getFlag(COMPACTION_MODEL_FLAG);
|
|
746
|
+
if (typeof rawFlag === "string" && rawFlag.trim()) {
|
|
747
|
+
const selector = normalizeSelector(rawFlag);
|
|
748
|
+
return selector ? { hasSelectors: true, source: "flag" } : { hasSelectors: false };
|
|
749
|
+
}
|
|
750
|
+
} catch {}
|
|
751
|
+
const agentDir = activeAgentDir(ctx);
|
|
752
|
+
const candidates = [];
|
|
753
|
+
if (isProjectPolicyTrusted(ctx, agentDir)) {
|
|
754
|
+
candidates.push({ path: join(ctx.cwd, CONFIG_DIR_NAME, PROJECT_POLICY), source: "project-policy" });
|
|
755
|
+
}
|
|
756
|
+
candidates.push({ path: join(agentDir, PROJECT_POLICY), source: "agent-policy" });
|
|
757
|
+
for (const candidate of candidates) {
|
|
758
|
+
const selectors = readPolicySelectors(candidate.path);
|
|
759
|
+
if (selectors !== undefined) {
|
|
760
|
+
return selectors.length > 0 ? { hasSelectors: true, source: candidate.source } : { hasSelectors: false };
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
return { hasSelectors: false };
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// protocol.ts
|
|
767
|
+
var RESPONSES_APIS = new Set([
|
|
768
|
+
"openai-responses",
|
|
769
|
+
"openai-codex-responses",
|
|
770
|
+
"azure-openai-responses"
|
|
771
|
+
]);
|
|
772
|
+
var MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
|
|
773
|
+
var MAX_ITEM_BYTES = 2 * 1024 * 1024;
|
|
774
|
+
var REMOTE_RETAINED_BYTES = 8 * 1024 * 1024;
|
|
775
|
+
var REMOTE_RETAINED_CHARS = 64000 * 4;
|
|
776
|
+
function isObject(value) {
|
|
777
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
778
|
+
}
|
|
779
|
+
function supportsResponsesApi(model) {
|
|
780
|
+
return model !== undefined && RESPONSES_APIS.has(String(model.api));
|
|
781
|
+
}
|
|
782
|
+
function resolveNativeProtocol(model) {
|
|
783
|
+
if (!supportsResponsesApi(model))
|
|
784
|
+
return;
|
|
785
|
+
return model.api === "openai-codex-responses" ? "remote-v2" : "responses-compact";
|
|
786
|
+
}
|
|
787
|
+
function isCompactionItem(value) {
|
|
788
|
+
return isObject(value) && value.type === "compaction" && typeof value.encrypted_content === "string" && value.encrypted_content.length > 0;
|
|
789
|
+
}
|
|
790
|
+
function byteLength(value) {
|
|
791
|
+
return Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
792
|
+
}
|
|
793
|
+
function validateItemSize(item) {
|
|
794
|
+
if (byteLength(item) > MAX_ITEM_BYTES)
|
|
795
|
+
throw new Error("Native compaction output item exceeded 2 MiB");
|
|
796
|
+
}
|
|
797
|
+
function validateCompactedOutput(value) {
|
|
798
|
+
if (!isObject(value) || !Array.isArray(value.output) || value.output.length === 0) {
|
|
799
|
+
throw new Error("Responses Compact returned an invalid output window");
|
|
800
|
+
}
|
|
801
|
+
const output = value.output.map((item) => {
|
|
802
|
+
if (!isObject(item))
|
|
803
|
+
throw new Error("Responses Compact returned a non-object output item");
|
|
804
|
+
validateItemSize(item);
|
|
805
|
+
return structuredClone(item);
|
|
806
|
+
});
|
|
807
|
+
const checkpoints = output.filter(isCompactionItem);
|
|
808
|
+
if (checkpoints.length !== 1) {
|
|
809
|
+
throw new Error(`Responses Compact returned ${checkpoints.length} compaction items; expected exactly one`);
|
|
810
|
+
}
|
|
811
|
+
return output;
|
|
812
|
+
}
|
|
813
|
+
async function readJsonBounded(response, signal) {
|
|
814
|
+
const declared = Number(response.headers.get("content-length"));
|
|
815
|
+
if (Number.isFinite(declared) && declared > MAX_RESPONSE_BYTES) {
|
|
816
|
+
await response.body?.cancel().catch(() => {
|
|
817
|
+
return;
|
|
818
|
+
});
|
|
819
|
+
throw new Error("Native compaction response exceeded 8 MiB");
|
|
820
|
+
}
|
|
821
|
+
if (!response.body)
|
|
822
|
+
throw new Error("Native compaction response did not contain a body");
|
|
823
|
+
const reader = response.body.getReader();
|
|
824
|
+
const chunks = [];
|
|
825
|
+
let total = 0;
|
|
826
|
+
const onAbort = () => void reader.cancel(new DOMException("Compaction aborted", "AbortError")).catch(() => {
|
|
827
|
+
return;
|
|
828
|
+
});
|
|
829
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
830
|
+
try {
|
|
831
|
+
while (true) {
|
|
832
|
+
if (signal.aborted)
|
|
833
|
+
throw new DOMException("Compaction aborted", "AbortError");
|
|
834
|
+
const { done, value } = await reader.read();
|
|
835
|
+
if (done)
|
|
836
|
+
break;
|
|
837
|
+
total += value.byteLength;
|
|
838
|
+
if (total > MAX_RESPONSE_BYTES)
|
|
839
|
+
throw new Error("Native compaction response exceeded 8 MiB");
|
|
840
|
+
chunks.push(value);
|
|
841
|
+
}
|
|
842
|
+
} finally {
|
|
843
|
+
signal.removeEventListener("abort", onAbort);
|
|
844
|
+
reader.releaseLock();
|
|
845
|
+
}
|
|
846
|
+
const bytes = new Uint8Array(total);
|
|
847
|
+
let offset = 0;
|
|
848
|
+
for (const chunk of chunks) {
|
|
849
|
+
bytes.set(chunk, offset);
|
|
850
|
+
offset += chunk.byteLength;
|
|
851
|
+
}
|
|
852
|
+
try {
|
|
853
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
854
|
+
} catch {
|
|
855
|
+
throw new Error("Native compaction response returned malformed JSON");
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
function requestUrl(input) {
|
|
859
|
+
return new URL(input instanceof Request ? input.url : String(input));
|
|
860
|
+
}
|
|
861
|
+
function responsesCompactUrl(input) {
|
|
862
|
+
const original = requestUrl(input);
|
|
863
|
+
if (!original.pathname.endsWith("/responses")) {
|
|
864
|
+
throw new Error("Provider request URL does not end with /responses");
|
|
865
|
+
}
|
|
866
|
+
const compact = new URL(original);
|
|
867
|
+
compact.pathname = `${compact.pathname}/compact`;
|
|
868
|
+
if (compact.origin !== original.origin)
|
|
869
|
+
throw new Error("Responses Compact URL changed origin");
|
|
870
|
+
return compact;
|
|
871
|
+
}
|
|
872
|
+
function mergedHeaders(input, init) {
|
|
873
|
+
const headers = new Headers(input instanceof Request ? input.headers : undefined);
|
|
874
|
+
new Headers(init?.headers).forEach((value, name) => headers.set(name, value));
|
|
875
|
+
headers.delete("content-encoding");
|
|
876
|
+
headers.delete("content-length");
|
|
877
|
+
headers.set("accept", "application/json");
|
|
878
|
+
headers.set("content-type", "application/json");
|
|
879
|
+
return headers;
|
|
880
|
+
}
|
|
881
|
+
function compactPayload(payload) {
|
|
882
|
+
const allowed = [
|
|
883
|
+
"model",
|
|
884
|
+
"input",
|
|
885
|
+
"instructions",
|
|
886
|
+
"previous_response_id",
|
|
887
|
+
"prompt_cache_key",
|
|
888
|
+
"prompt_cache_retention",
|
|
889
|
+
"service_tier"
|
|
890
|
+
];
|
|
891
|
+
const result = {};
|
|
892
|
+
for (const key of allowed) {
|
|
893
|
+
if (Object.hasOwn(payload, key) && payload[key] !== undefined)
|
|
894
|
+
result[key] = structuredClone(payload[key]);
|
|
895
|
+
}
|
|
896
|
+
if (typeof result.model !== "string" || !Array.isArray(result.input)) {
|
|
897
|
+
throw new Error("Provider did not expose a valid Responses payload for compaction");
|
|
898
|
+
}
|
|
899
|
+
return result;
|
|
900
|
+
}
|
|
901
|
+
function errorResponse(error) {
|
|
902
|
+
return Response.json({ error: { message: error instanceof Error ? error.message : String(error), type: "invalid_request_error" } }, { status: 400 });
|
|
903
|
+
}
|
|
904
|
+
function rawUsage(response) {
|
|
905
|
+
if (!isObject(response.usage))
|
|
906
|
+
throw new Error("Native compaction response is missing usage");
|
|
907
|
+
return structuredClone(response.usage);
|
|
908
|
+
}
|
|
909
|
+
function syntheticCompletion(response, payload) {
|
|
910
|
+
const completed = {
|
|
911
|
+
id: typeof response.id === "string" ? response.id : "resp_pi_provider_compaction",
|
|
912
|
+
object: "response",
|
|
913
|
+
created_at: typeof response.created_at === "number" ? response.created_at : Math.floor(Date.now() / 1000),
|
|
914
|
+
status: "completed",
|
|
915
|
+
model: payload.model,
|
|
916
|
+
output: [],
|
|
917
|
+
parallel_tool_calls: false,
|
|
918
|
+
tool_choice: "auto",
|
|
919
|
+
tools: [],
|
|
920
|
+
usage: rawUsage(response)
|
|
921
|
+
};
|
|
922
|
+
const body = [
|
|
923
|
+
{ type: "response.created", response: { ...completed, status: "in_progress" } },
|
|
924
|
+
{ type: "response.completed", response: completed }
|
|
925
|
+
].map((event) => `data: ${JSON.stringify(event)}
|
|
926
|
+
|
|
927
|
+
`).join("");
|
|
928
|
+
return new Response(body, { status: 200, headers: { "content-type": "text/event-stream" } });
|
|
929
|
+
}
|
|
930
|
+
async function collectProviderUsage(stream, signal) {
|
|
931
|
+
let usage;
|
|
932
|
+
for await (const event of stream) {
|
|
933
|
+
if (signal.aborted)
|
|
934
|
+
throw new DOMException("Compaction aborted", "AbortError");
|
|
935
|
+
if (event.type === "error")
|
|
936
|
+
throw new Error(event.error.errorMessage ?? "Native compaction provider request failed");
|
|
937
|
+
if (event.type === "done")
|
|
938
|
+
usage = event.message.usage;
|
|
939
|
+
}
|
|
940
|
+
if (!usage)
|
|
941
|
+
throw new Error("Native compaction provider stream ended without usage");
|
|
942
|
+
return usage;
|
|
943
|
+
}
|
|
944
|
+
async function requestResponsesCompact(request) {
|
|
945
|
+
let preparedPayload;
|
|
946
|
+
let compacted;
|
|
947
|
+
let bridgeError;
|
|
948
|
+
let dispatches = 0;
|
|
949
|
+
const baseFetch = request.fetch ?? globalThis.fetch;
|
|
950
|
+
const bridgeFetch = async (input, init) => {
|
|
951
|
+
if (request.signal.aborted)
|
|
952
|
+
throw new DOMException("Compaction aborted", "AbortError");
|
|
953
|
+
dispatches += 1;
|
|
954
|
+
if (dispatches !== 1 || !preparedPayload) {
|
|
955
|
+
bridgeError = new Error("Responses Compact provider dispatched an unexpected additional request");
|
|
956
|
+
return errorResponse(bridgeError);
|
|
957
|
+
}
|
|
958
|
+
try {
|
|
959
|
+
const compactUrl = responsesCompactUrl(input);
|
|
960
|
+
const response = await baseFetch(compactUrl, {
|
|
961
|
+
...init,
|
|
962
|
+
method: "POST",
|
|
963
|
+
headers: mergedHeaders(input, init),
|
|
964
|
+
body: JSON.stringify(compactPayload(preparedPayload)),
|
|
965
|
+
signal: request.signal
|
|
966
|
+
});
|
|
967
|
+
if (!response.ok)
|
|
968
|
+
return response;
|
|
969
|
+
const parsed = await readJsonBounded(response, request.signal);
|
|
970
|
+
if (!isObject(parsed))
|
|
971
|
+
throw new Error("Responses Compact returned a non-object response");
|
|
972
|
+
compacted = { response: parsed, output: validateCompactedOutput(parsed) };
|
|
973
|
+
return syntheticCompletion(parsed, preparedPayload);
|
|
974
|
+
} catch (error) {
|
|
975
|
+
bridgeError = error;
|
|
976
|
+
return errorResponse(error);
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
const stream = request.provider.stream(request.model, request.context, {
|
|
980
|
+
apiKey: request.apiKey,
|
|
981
|
+
headers: request.headers,
|
|
982
|
+
env: request.env,
|
|
983
|
+
signal: request.signal,
|
|
984
|
+
transport: "sse",
|
|
985
|
+
cacheRetention: "none",
|
|
986
|
+
maxRetries: 0,
|
|
987
|
+
fetch: bridgeFetch,
|
|
988
|
+
onPayload: (payload) => {
|
|
989
|
+
if (!isObject(payload))
|
|
990
|
+
throw new Error("Responses provider exposed a non-object payload");
|
|
991
|
+
preparedPayload = { ...structuredClone(payload), input: structuredClone(request.input) };
|
|
992
|
+
return preparedPayload;
|
|
993
|
+
}
|
|
994
|
+
});
|
|
995
|
+
const usage = await collectProviderUsage(stream, request.signal).catch((error) => {
|
|
996
|
+
if (bridgeError)
|
|
997
|
+
throw bridgeError;
|
|
998
|
+
throw error;
|
|
999
|
+
});
|
|
1000
|
+
if (bridgeError)
|
|
1001
|
+
throw bridgeError;
|
|
1002
|
+
if (!compacted || dispatches !== 1)
|
|
1003
|
+
throw new Error("Responses Compact did not complete exactly one request");
|
|
1004
|
+
return { output: compacted.output, usage };
|
|
1005
|
+
}
|
|
1006
|
+
async function collectCompactionSse(stream, signal) {
|
|
1007
|
+
const reader = stream.getReader();
|
|
1008
|
+
const decoder = new TextDecoder;
|
|
1009
|
+
let bytes = 0;
|
|
1010
|
+
let pending = "";
|
|
1011
|
+
let dataLines = [];
|
|
1012
|
+
let completed = false;
|
|
1013
|
+
const items = new Map;
|
|
1014
|
+
const onAbort = () => void reader.cancel(new DOMException("Compaction aborted", "AbortError")).catch(() => {
|
|
1015
|
+
return;
|
|
1016
|
+
});
|
|
1017
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1018
|
+
const dispatch = () => {
|
|
1019
|
+
if (dataLines.length === 0)
|
|
1020
|
+
return;
|
|
1021
|
+
const data = dataLines.join(`
|
|
1022
|
+
`);
|
|
1023
|
+
dataLines = [];
|
|
1024
|
+
if (data === "[DONE]")
|
|
1025
|
+
return;
|
|
1026
|
+
let event;
|
|
1027
|
+
try {
|
|
1028
|
+
event = JSON.parse(data);
|
|
1029
|
+
} catch {
|
|
1030
|
+
throw new Error("Remote V2 returned malformed SSE JSON");
|
|
1031
|
+
}
|
|
1032
|
+
if (!isObject(event))
|
|
1033
|
+
return;
|
|
1034
|
+
if (event.type === "response.completed")
|
|
1035
|
+
completed = true;
|
|
1036
|
+
const candidates = [];
|
|
1037
|
+
if (event.type === "response.output_item.done")
|
|
1038
|
+
candidates.push(event.item);
|
|
1039
|
+
if (event.type === "response.completed" && isObject(event.response) && Array.isArray(event.response.output)) {
|
|
1040
|
+
candidates.push(...event.response.output);
|
|
1041
|
+
}
|
|
1042
|
+
for (const candidate of candidates) {
|
|
1043
|
+
if (!isCompactionItem(candidate))
|
|
1044
|
+
continue;
|
|
1045
|
+
validateItemSize(candidate);
|
|
1046
|
+
items.set(JSON.stringify(candidate), structuredClone(candidate));
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
const processLine = (line) => {
|
|
1050
|
+
if (line === "")
|
|
1051
|
+
return dispatch();
|
|
1052
|
+
if (line.startsWith(":"))
|
|
1053
|
+
return;
|
|
1054
|
+
if (line.startsWith("data:"))
|
|
1055
|
+
dataLines.push(line.slice(5).replace(/^ /, ""));
|
|
1056
|
+
};
|
|
1057
|
+
try {
|
|
1058
|
+
while (true) {
|
|
1059
|
+
if (signal.aborted)
|
|
1060
|
+
throw new DOMException("Compaction aborted", "AbortError");
|
|
1061
|
+
const { done, value } = await reader.read();
|
|
1062
|
+
if (done)
|
|
1063
|
+
break;
|
|
1064
|
+
bytes += value.byteLength;
|
|
1065
|
+
if (bytes > MAX_RESPONSE_BYTES)
|
|
1066
|
+
throw new Error("Remote V2 stream exceeded 8 MiB");
|
|
1067
|
+
pending += decoder.decode(value, { stream: true });
|
|
1068
|
+
let newline = pending.indexOf(`
|
|
1069
|
+
`);
|
|
1070
|
+
while (newline >= 0) {
|
|
1071
|
+
const raw = pending.slice(0, newline);
|
|
1072
|
+
pending = pending.slice(newline + 1);
|
|
1073
|
+
processLine(raw.endsWith("\r") ? raw.slice(0, -1) : raw);
|
|
1074
|
+
newline = pending.indexOf(`
|
|
1075
|
+
`);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
pending += decoder.decode();
|
|
1079
|
+
if (pending)
|
|
1080
|
+
processLine(pending.endsWith("\r") ? pending.slice(0, -1) : pending);
|
|
1081
|
+
dispatch();
|
|
1082
|
+
} finally {
|
|
1083
|
+
signal.removeEventListener("abort", onAbort);
|
|
1084
|
+
reader.releaseLock();
|
|
1085
|
+
}
|
|
1086
|
+
if (!completed)
|
|
1087
|
+
throw new Error("Remote V2 stream ended without response.completed");
|
|
1088
|
+
if (items.size !== 1)
|
|
1089
|
+
throw new Error(`Remote V2 returned ${items.size} distinct compaction items; expected exactly one`);
|
|
1090
|
+
return { item: [...items.values()][0] };
|
|
1091
|
+
}
|
|
1092
|
+
function retainedUserMessage(item) {
|
|
1093
|
+
return (item.type === undefined || item.type === "message") && item.role === "user" && Array.isArray(item.content);
|
|
1094
|
+
}
|
|
1095
|
+
function buildRemoteV2History(input, item) {
|
|
1096
|
+
validateItemSize(item);
|
|
1097
|
+
let remainingBytes = REMOTE_RETAINED_BYTES - byteLength(item);
|
|
1098
|
+
let remainingChars = REMOTE_RETAINED_CHARS;
|
|
1099
|
+
const kept = [];
|
|
1100
|
+
for (let index = input.length - 1;index >= 0; index--) {
|
|
1101
|
+
const candidate = input[index];
|
|
1102
|
+
if (!retainedUserMessage(candidate))
|
|
1103
|
+
continue;
|
|
1104
|
+
const bytes = byteLength(candidate);
|
|
1105
|
+
if (bytes > remainingBytes)
|
|
1106
|
+
continue;
|
|
1107
|
+
const chars = JSON.stringify(candidate).length;
|
|
1108
|
+
if (chars > remainingChars)
|
|
1109
|
+
continue;
|
|
1110
|
+
kept.push(structuredClone(candidate));
|
|
1111
|
+
remainingBytes -= bytes;
|
|
1112
|
+
remainingChars -= chars;
|
|
1113
|
+
if (remainingBytes < 256 || remainingChars < 64)
|
|
1114
|
+
break;
|
|
1115
|
+
}
|
|
1116
|
+
return [...kept.reverse(), structuredClone(item)];
|
|
1117
|
+
}
|
|
1118
|
+
async function requestRemoteV2(request) {
|
|
1119
|
+
const baseFetch = request.fetch ?? globalThis.fetch;
|
|
1120
|
+
let sentInput;
|
|
1121
|
+
const inspections = [];
|
|
1122
|
+
const inspectedFetch = async (input, init) => {
|
|
1123
|
+
const response = await baseFetch(input, init);
|
|
1124
|
+
if (!response.ok || !response.body)
|
|
1125
|
+
return response;
|
|
1126
|
+
const [providerBody, inspectionBody] = response.body.tee();
|
|
1127
|
+
inspections.push(collectCompactionSse(inspectionBody, request.signal));
|
|
1128
|
+
return new Response(providerBody, {
|
|
1129
|
+
status: response.status,
|
|
1130
|
+
statusText: response.statusText,
|
|
1131
|
+
headers: response.headers
|
|
1132
|
+
});
|
|
1133
|
+
};
|
|
1134
|
+
const stream = request.provider.stream(request.model, request.context, {
|
|
1135
|
+
apiKey: request.apiKey,
|
|
1136
|
+
headers: request.headers,
|
|
1137
|
+
env: request.env,
|
|
1138
|
+
signal: request.signal,
|
|
1139
|
+
transport: "sse",
|
|
1140
|
+
cacheRetention: "none",
|
|
1141
|
+
maxRetries: 0,
|
|
1142
|
+
fetch: inspectedFetch,
|
|
1143
|
+
onPayload: (payload) => {
|
|
1144
|
+
if (!isObject(payload))
|
|
1145
|
+
throw new Error("Codex provider exposed a non-object payload");
|
|
1146
|
+
sentInput = structuredClone(request.input);
|
|
1147
|
+
return { ...structuredClone(payload), input: [...sentInput, { type: "compaction_trigger" }] };
|
|
1148
|
+
}
|
|
1149
|
+
});
|
|
1150
|
+
const usage = await collectProviderUsage(stream, request.signal);
|
|
1151
|
+
if (!sentInput || inspections.length !== 1) {
|
|
1152
|
+
throw new Error(`Remote V2 observed ${inspections.length} successful provider responses; expected exactly one`);
|
|
1153
|
+
}
|
|
1154
|
+
const inspection = await inspections[0];
|
|
1155
|
+
return { output: buildRemoteV2History(sentInput, inspection.item), usage };
|
|
1156
|
+
}
|
|
1157
|
+
async function requestProviderCompaction(request) {
|
|
1158
|
+
return request.protocol === "remote-v2" ? requestRemoteV2(request) : requestResponsesCompact(request);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// index.ts
|
|
1162
|
+
var NATIVE_COMPACTION_TYPE = "pi-provider-compaction/openai-responses";
|
|
1163
|
+
var NATIVE_COMPACTION_VERSION = 2;
|
|
1164
|
+
var NATIVE_COMPACTION_SUMMARY = "This context was compacted using the provider's native OpenAI Responses state.";
|
|
1165
|
+
var TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]);
|
|
1166
|
+
function supportsNativeCompaction(model) {
|
|
1167
|
+
return supportsResponsesApi(model);
|
|
1168
|
+
}
|
|
1169
|
+
function readUsage(value) {
|
|
1170
|
+
if (!isObject(value) || !isObject(value.cost))
|
|
1171
|
+
return;
|
|
1172
|
+
for (const key of ["input", "output", "cacheRead", "cacheWrite", "totalTokens"]) {
|
|
1173
|
+
if (typeof value[key] !== "number" || !Number.isFinite(value[key]))
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
for (const key of ["input", "output", "cacheRead", "cacheWrite", "total"]) {
|
|
1177
|
+
if (typeof value.cost[key] !== "number" || !Number.isFinite(value.cost[key]))
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
return value;
|
|
1181
|
+
}
|
|
1182
|
+
function readIdentity(value) {
|
|
1183
|
+
if (!isObject(value) || typeof value.endpoint !== "string" || !value.endpoint)
|
|
1184
|
+
return;
|
|
1185
|
+
for (const key of ["organization", "project", "accountId", "azureApiVersion", "azureDeployment"]) {
|
|
1186
|
+
if (value[key] !== undefined && typeof value[key] !== "string")
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
return {
|
|
1190
|
+
endpoint: value.endpoint,
|
|
1191
|
+
organization: value.organization,
|
|
1192
|
+
project: value.project,
|
|
1193
|
+
accountId: value.accountId,
|
|
1194
|
+
azureApiVersion: value.azureApiVersion,
|
|
1195
|
+
azureDeployment: value.azureDeployment
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
function readNativeCompactionDetails(value) {
|
|
1199
|
+
if (!isObject(value) || value.type !== NATIVE_COMPACTION_TYPE)
|
|
1200
|
+
return;
|
|
1201
|
+
if (value.version !== 1 && value.version !== NATIVE_COMPACTION_VERSION)
|
|
1202
|
+
return;
|
|
1203
|
+
if (typeof value.provider !== "string" || typeof value.api !== "string" || typeof value.model !== "string")
|
|
1204
|
+
return;
|
|
1205
|
+
if (!Array.isArray(value.output) || !value.output.every(isObject) || !value.output.some(isCompactionItem))
|
|
1206
|
+
return;
|
|
1207
|
+
const protocol = value.version === 1 ? "responses-compact" : value.protocol === "responses-compact" || value.protocol === "remote-v2" ? value.protocol : undefined;
|
|
1208
|
+
if (!protocol)
|
|
1209
|
+
return;
|
|
1210
|
+
const strings = (candidate) => candidate === undefined ? undefined : Array.isArray(candidate) && candidate.every((item) => typeof item === "string") ? [...candidate] : undefined;
|
|
1211
|
+
return {
|
|
1212
|
+
type: NATIVE_COMPACTION_TYPE,
|
|
1213
|
+
version: value.version,
|
|
1214
|
+
provider: value.provider,
|
|
1215
|
+
api: value.api,
|
|
1216
|
+
model: value.model,
|
|
1217
|
+
protocol,
|
|
1218
|
+
identity: value.version === 1 ? undefined : readIdentity(value.identity),
|
|
1219
|
+
output: structuredClone(value.output),
|
|
1220
|
+
nativeUsage: readUsage(value.nativeUsage ?? value.usage),
|
|
1221
|
+
usage: readUsage(value.usage),
|
|
1222
|
+
portableUsage: readUsage(value.portableUsage),
|
|
1223
|
+
readFiles: strings(value.readFiles),
|
|
1224
|
+
modifiedFiles: strings(value.modifiedFiles)
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
function identityEqual(left, right) {
|
|
1228
|
+
if (!left)
|
|
1229
|
+
return true;
|
|
1230
|
+
if (!right)
|
|
1231
|
+
return false;
|
|
1232
|
+
return left.endpoint === right.endpoint && left.organization === right.organization && left.project === right.project && left.accountId === right.accountId && left.azureApiVersion === right.azureApiVersion && left.azureDeployment === right.azureDeployment;
|
|
1233
|
+
}
|
|
1234
|
+
function findLatestNativeCompaction(entries, model, identity) {
|
|
1235
|
+
if (!supportsNativeCompaction(model))
|
|
1236
|
+
return;
|
|
1237
|
+
for (let index = entries.length - 1;index >= 0; index--) {
|
|
1238
|
+
const entry = entries[index];
|
|
1239
|
+
if (entry.type !== "compaction")
|
|
1240
|
+
continue;
|
|
1241
|
+
const details = readNativeCompactionDetails(entry.details);
|
|
1242
|
+
if (!details)
|
|
1243
|
+
return;
|
|
1244
|
+
if (details.provider !== model.provider || details.api !== model.api || details.model !== model.id)
|
|
1245
|
+
return;
|
|
1246
|
+
if (!identityEqual(details.identity, identity))
|
|
1247
|
+
return;
|
|
1248
|
+
return { ...details, entryId: entry.id };
|
|
1249
|
+
}
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
function responseItemsFromMessages(model, messages) {
|
|
1253
|
+
const input = convertResponsesMessages(model, { messages: convertToLlm(messages), tools: [] }, TOOL_CALL_PROVIDERS, { includeSystemPrompt: false });
|
|
1254
|
+
return input.filter(isObject);
|
|
1255
|
+
}
|
|
1256
|
+
function responseItemsFromEntries(model, entries) {
|
|
1257
|
+
return responseItemsFromMessages(model, entries.flatMap((entry) => sessionEntryToContextMessages(entry)));
|
|
1258
|
+
}
|
|
1259
|
+
function nativeRequestInput(model, branchEntries, activeEntries, previous) {
|
|
1260
|
+
if (previous) {
|
|
1261
|
+
const previousIndex = branchEntries.findIndex((entry) => entry.id === previous.entryId);
|
|
1262
|
+
if (previousIndex >= 0) {
|
|
1263
|
+
return [...structuredClone(previous.output), ...responseItemsFromEntries(model, branchEntries.slice(previousIndex + 1))];
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
return responseItemsFromEntries(model, activeEntries);
|
|
1267
|
+
}
|
|
1268
|
+
function authHeaders(apiKey, headers) {
|
|
1269
|
+
const result = { "content-type": "application/json" };
|
|
1270
|
+
let hasAuthorization = false;
|
|
1271
|
+
for (const [name, value] of Object.entries(headers ?? {})) {
|
|
1272
|
+
if (value === null)
|
|
1273
|
+
continue;
|
|
1274
|
+
result[name] = value;
|
|
1275
|
+
if (name.toLowerCase() === "authorization" && value.trim())
|
|
1276
|
+
hasAuthorization = true;
|
|
1277
|
+
}
|
|
1278
|
+
if (!hasAuthorization && apiKey)
|
|
1279
|
+
result.authorization = `Bearer ${apiKey}`;
|
|
1280
|
+
return result;
|
|
1281
|
+
}
|
|
1282
|
+
async function compactOpenAIResponses(options) {
|
|
1283
|
+
const endpoint = `${options.baseUrl.replace(/\/+$/, "")}/responses/compact`;
|
|
1284
|
+
const body = { model: options.model.id, input: options.input };
|
|
1285
|
+
if (options.instructions)
|
|
1286
|
+
body.instructions = options.instructions;
|
|
1287
|
+
const response = await (options.fetchImpl ?? fetch)(endpoint, {
|
|
1288
|
+
method: "POST",
|
|
1289
|
+
headers: authHeaders(options.apiKey, options.headers),
|
|
1290
|
+
body: JSON.stringify(body),
|
|
1291
|
+
signal: options.signal
|
|
1292
|
+
});
|
|
1293
|
+
if (!response.ok) {
|
|
1294
|
+
const detail = await response.text().catch(() => "");
|
|
1295
|
+
throw new Error(`OpenAI Responses compact request failed (${response.status}${detail ? `: ${detail.slice(0, 400)}` : ""})`);
|
|
1296
|
+
}
|
|
1297
|
+
const result = await response.json();
|
|
1298
|
+
if (!isObject(result) || !Array.isArray(result.output) || !result.output.every(isObject) || !result.output.some(isCompactionItem)) {
|
|
1299
|
+
throw new Error("OpenAI Responses compact response did not contain opaque compaction state");
|
|
1300
|
+
}
|
|
1301
|
+
const usage = compactUsage(options.model, result);
|
|
1302
|
+
return {
|
|
1303
|
+
type: NATIVE_COMPACTION_TYPE,
|
|
1304
|
+
version: NATIVE_COMPACTION_VERSION,
|
|
1305
|
+
provider: options.model.provider,
|
|
1306
|
+
api: options.model.api,
|
|
1307
|
+
model: options.model.id,
|
|
1308
|
+
protocol: "responses-compact",
|
|
1309
|
+
output: structuredClone(result.output),
|
|
1310
|
+
nativeUsage: usage,
|
|
1311
|
+
usage
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
function compactUsage(model, result) {
|
|
1315
|
+
const usage = result.usage;
|
|
1316
|
+
if (!isObject(usage))
|
|
1317
|
+
return;
|
|
1318
|
+
const cached = nestedNumber(usage.input_tokens_details, "cached_tokens");
|
|
1319
|
+
const cacheWrite = nestedNumber(usage.input_tokens_details, "cache_write_tokens");
|
|
1320
|
+
const mapped = {
|
|
1321
|
+
input: Math.max(0, readNumber(usage.input_tokens) - cached - cacheWrite),
|
|
1322
|
+
output: readNumber(usage.output_tokens),
|
|
1323
|
+
cacheRead: cached,
|
|
1324
|
+
cacheWrite,
|
|
1325
|
+
reasoning: nestedNumber(usage.output_tokens_details, "reasoning_tokens"),
|
|
1326
|
+
totalTokens: readNumber(usage.total_tokens),
|
|
1327
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }
|
|
1328
|
+
};
|
|
1329
|
+
try {
|
|
1330
|
+
calculateCost(model, mapped);
|
|
1331
|
+
} catch {}
|
|
1332
|
+
return mapped;
|
|
1333
|
+
}
|
|
1334
|
+
function readNumber(value) {
|
|
1335
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
1336
|
+
}
|
|
1337
|
+
function nestedNumber(value, key) {
|
|
1338
|
+
return isObject(value) ? readNumber(value[key]) : 0;
|
|
1339
|
+
}
|
|
1340
|
+
function stableStringify(value) {
|
|
1341
|
+
if (Array.isArray(value))
|
|
1342
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
1343
|
+
if (isObject(value)) {
|
|
1344
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
|
|
1345
|
+
}
|
|
1346
|
+
return JSON.stringify(value) ?? "null";
|
|
1347
|
+
}
|
|
1348
|
+
function itemMatches(left, right) {
|
|
1349
|
+
const leftId = typeof left.id === "string" ? left.id : undefined;
|
|
1350
|
+
const rightId = typeof right.id === "string" ? right.id : undefined;
|
|
1351
|
+
if (leftId && rightId)
|
|
1352
|
+
return leftId === rightId;
|
|
1353
|
+
const leftCallId = typeof left.call_id === "string" ? left.call_id : undefined;
|
|
1354
|
+
const rightCallId = typeof right.call_id === "string" ? right.call_id : undefined;
|
|
1355
|
+
if (leftCallId && rightCallId)
|
|
1356
|
+
return leftCallId === rightCallId && left.type === right.type;
|
|
1357
|
+
if (left.type === "message" && right.type === "message") {
|
|
1358
|
+
return left.role === right.role && stableStringify(left.content) === stableStringify(right.content);
|
|
1359
|
+
}
|
|
1360
|
+
return stableStringify(left) === stableStringify(right);
|
|
1361
|
+
}
|
|
1362
|
+
function findLastSubsequence(items, sequence) {
|
|
1363
|
+
if (sequence.length === 0)
|
|
1364
|
+
return -1;
|
|
1365
|
+
for (let start = items.length - sequence.length;start >= 0; start--) {
|
|
1366
|
+
if (sequence.every((item, offset) => itemMatches(items[start + offset], item)))
|
|
1367
|
+
return start;
|
|
1368
|
+
}
|
|
1369
|
+
return -1;
|
|
1370
|
+
}
|
|
1371
|
+
function rewriteResponsesPayload(payload, details, postCompactionItems) {
|
|
1372
|
+
if (!isObject(payload) || !Array.isArray(payload.input))
|
|
1373
|
+
return payload;
|
|
1374
|
+
const currentInput = payload.input.filter(isObject);
|
|
1375
|
+
if (currentInput.length !== payload.input.length)
|
|
1376
|
+
return payload;
|
|
1377
|
+
const postStart = findLastSubsequence(currentInput, postCompactionItems);
|
|
1378
|
+
if (postStart >= 0)
|
|
1379
|
+
return { ...payload, input: [...structuredClone(details.output), ...currentInput.slice(postStart)] };
|
|
1380
|
+
const retainedOutput = details.output.filter((item) => item.type !== "compaction");
|
|
1381
|
+
const retainedStart = findLastSubsequence(currentInput, retainedOutput);
|
|
1382
|
+
if (retainedStart >= 0) {
|
|
1383
|
+
return { ...payload, input: [...structuredClone(details.output), ...currentInput.slice(retainedStart + retainedOutput.length)] };
|
|
1384
|
+
}
|
|
1385
|
+
return { ...payload, input: [...structuredClone(details.output), ...structuredClone(postCompactionItems)] };
|
|
1386
|
+
}
|
|
1387
|
+
function genericCompactionSelected(pi, ctx) {
|
|
1388
|
+
return resolveCompactionModelPolicy(pi, ctx).hasSelectors;
|
|
1389
|
+
}
|
|
1390
|
+
function normalizedEndpoint(value) {
|
|
1391
|
+
if (!value)
|
|
1392
|
+
return "";
|
|
1393
|
+
try {
|
|
1394
|
+
const url = new URL(value);
|
|
1395
|
+
url.username = "";
|
|
1396
|
+
url.password = "";
|
|
1397
|
+
url.search = "";
|
|
1398
|
+
url.hash = "";
|
|
1399
|
+
return url.toString().replace(/\/+$/, "");
|
|
1400
|
+
} catch {
|
|
1401
|
+
return value.replace(/\/+$/, "");
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
function headerValue(headers, name) {
|
|
1405
|
+
const match = Object.entries(headers ?? {}).find(([key]) => key.toLowerCase() === name.toLowerCase());
|
|
1406
|
+
return typeof match?.[1] === "string" && match[1] ? match[1] : undefined;
|
|
1407
|
+
}
|
|
1408
|
+
function codexAccountId(apiKey) {
|
|
1409
|
+
if (!apiKey)
|
|
1410
|
+
return;
|
|
1411
|
+
const parts = apiKey.split(".");
|
|
1412
|
+
if (parts.length !== 3)
|
|
1413
|
+
return;
|
|
1414
|
+
try {
|
|
1415
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
|
|
1416
|
+
if (!isObject(payload))
|
|
1417
|
+
return;
|
|
1418
|
+
const auth = payload["https://api.openai.com/auth"];
|
|
1419
|
+
return isObject(auth) && typeof auth.chatgpt_account_id === "string" ? auth.chatgpt_account_id : undefined;
|
|
1420
|
+
} catch {
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
function azureDeployment(modelId, env) {
|
|
1425
|
+
const raw = env?.AZURE_OPENAI_DEPLOYMENT_NAME_MAP;
|
|
1426
|
+
if (!raw)
|
|
1427
|
+
return modelId;
|
|
1428
|
+
for (const entry of raw.split(",")) {
|
|
1429
|
+
const [candidate, deployment] = entry.split("=", 2).map((part) => part?.trim());
|
|
1430
|
+
if (candidate === modelId && deployment)
|
|
1431
|
+
return deployment;
|
|
1432
|
+
}
|
|
1433
|
+
return modelId;
|
|
1434
|
+
}
|
|
1435
|
+
function routeIdentity(model, auth) {
|
|
1436
|
+
const azure = model.api === "azure-openai-responses";
|
|
1437
|
+
return {
|
|
1438
|
+
endpoint: normalizedEndpoint(auth.baseUrl ?? auth.env?.AZURE_OPENAI_BASE_URL ?? model.baseUrl),
|
|
1439
|
+
organization: headerValue(auth.headers, "openai-organization"),
|
|
1440
|
+
project: headerValue(auth.headers, "openai-project"),
|
|
1441
|
+
accountId: model.api === "openai-codex-responses" ? codexAccountId(auth.apiKey) : undefined,
|
|
1442
|
+
azureApiVersion: azure ? auth.env?.AZURE_OPENAI_API_VERSION || "v1" : undefined,
|
|
1443
|
+
azureDeployment: azure ? azureDeployment(model.id, auth.env) : undefined
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
function activeTools(pi) {
|
|
1447
|
+
const available = new Map(pi.getAllTools().map((tool) => [tool.name, tool]));
|
|
1448
|
+
return pi.getActiveTools().flatMap((name) => {
|
|
1449
|
+
const tool = available.get(name);
|
|
1450
|
+
return tool ? [{ name: tool.name, description: tool.description, parameters: tool.parameters }] : [];
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
function providerContext(pi, ctx, activeEntries) {
|
|
1454
|
+
const messages = activeEntries.flatMap((entry) => sessionEntryToContextMessages(entry));
|
|
1455
|
+
return {
|
|
1456
|
+
systemPrompt: ctx.getSystemPrompt(),
|
|
1457
|
+
messages: convertToLlm(messages),
|
|
1458
|
+
tools: activeTools(pi)
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
function cleanHeaders(headers) {
|
|
1462
|
+
if (!headers)
|
|
1463
|
+
return;
|
|
1464
|
+
return Object.fromEntries(Object.entries(headers).filter((entry) => entry[1] !== null));
|
|
1465
|
+
}
|
|
1466
|
+
function combineUsage(first, second) {
|
|
1467
|
+
if (!first)
|
|
1468
|
+
return second;
|
|
1469
|
+
if (!second)
|
|
1470
|
+
return first;
|
|
1471
|
+
return {
|
|
1472
|
+
input: first.input + second.input,
|
|
1473
|
+
output: first.output + second.output,
|
|
1474
|
+
cacheRead: first.cacheRead + second.cacheRead,
|
|
1475
|
+
cacheWrite: first.cacheWrite + second.cacheWrite,
|
|
1476
|
+
cacheWrite1h: first.cacheWrite1h !== undefined || second.cacheWrite1h !== undefined ? (first.cacheWrite1h ?? 0) + (second.cacheWrite1h ?? 0) : undefined,
|
|
1477
|
+
reasoning: first.reasoning !== undefined || second.reasoning !== undefined ? (first.reasoning ?? 0) + (second.reasoning ?? 0) : undefined,
|
|
1478
|
+
totalTokens: first.totalTokens + second.totalTokens,
|
|
1479
|
+
cost: {
|
|
1480
|
+
input: first.cost.input + second.cost.input,
|
|
1481
|
+
output: first.cost.output + second.cost.output,
|
|
1482
|
+
cacheRead: first.cost.cacheRead + second.cost.cacheRead,
|
|
1483
|
+
cacheWrite: first.cost.cacheWrite + second.cost.cacheWrite,
|
|
1484
|
+
total: first.cost.total + second.cost.total
|
|
1485
|
+
}
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
async function nativeAndPortableCompaction(pi, event, ctx) {
|
|
1489
|
+
const model = ctx.model;
|
|
1490
|
+
const protocol = resolveNativeProtocol(model);
|
|
1491
|
+
if (!model || !protocol || genericCompactionSelected(pi, ctx))
|
|
1492
|
+
return;
|
|
1493
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
1494
|
+
if (!auth.ok)
|
|
1495
|
+
return;
|
|
1496
|
+
const provider = ctx.modelRegistry.getProvider(model.provider);
|
|
1497
|
+
if (!provider)
|
|
1498
|
+
return;
|
|
1499
|
+
const identity = routeIdentity(model, auth);
|
|
1500
|
+
const branchEntries = event.branchEntries;
|
|
1501
|
+
const activeEntries = ctx.sessionManager.buildContextEntries();
|
|
1502
|
+
const previous = findLatestNativeCompaction(branchEntries, model, identity);
|
|
1503
|
+
const input = nativeRequestInput(model, branchEntries, activeEntries, previous);
|
|
1504
|
+
const native = await requestProviderCompaction({
|
|
1505
|
+
provider,
|
|
1506
|
+
model,
|
|
1507
|
+
context: providerContext(pi, ctx, activeEntries),
|
|
1508
|
+
input,
|
|
1509
|
+
protocol,
|
|
1510
|
+
apiKey: auth.apiKey,
|
|
1511
|
+
headers: auth.headers,
|
|
1512
|
+
env: auth.env,
|
|
1513
|
+
signal: event.signal
|
|
1514
|
+
});
|
|
1515
|
+
const portable = await compact(event.preparation, model, auth.apiKey, cleanHeaders(auth.headers), event.customInstructions, event.signal, ctx.thinkingLevel, provider.streamSimple.bind(provider), auth.env, undefined, undefined, ctx.sessionManager.getSessionId());
|
|
1516
|
+
const portableDetails = isObject(portable.details) ? portable.details : {};
|
|
1517
|
+
const details = {
|
|
1518
|
+
type: NATIVE_COMPACTION_TYPE,
|
|
1519
|
+
version: NATIVE_COMPACTION_VERSION,
|
|
1520
|
+
provider: model.provider,
|
|
1521
|
+
api: model.api,
|
|
1522
|
+
model: model.id,
|
|
1523
|
+
protocol,
|
|
1524
|
+
identity,
|
|
1525
|
+
output: native.output,
|
|
1526
|
+
nativeUsage: native.usage,
|
|
1527
|
+
portableUsage: portable.usage,
|
|
1528
|
+
readFiles: Array.isArray(portableDetails.readFiles) ? portableDetails.readFiles.filter((item) => typeof item === "string") : undefined,
|
|
1529
|
+
modifiedFiles: Array.isArray(portableDetails.modifiedFiles) ? portableDetails.modifiedFiles.filter((item) => typeof item === "string") : undefined
|
|
1530
|
+
};
|
|
1531
|
+
return {
|
|
1532
|
+
compaction: {
|
|
1533
|
+
...portable,
|
|
1534
|
+
usage: combineUsage(native.usage, portable.usage),
|
|
1535
|
+
details
|
|
1536
|
+
}
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
function pi_provider_compaction_default(pi) {
|
|
1540
|
+
pi.on("before_provider_request", async (event, ctx) => {
|
|
1541
|
+
const model = ctx.model;
|
|
1542
|
+
if (!supportsNativeCompaction(model))
|
|
1543
|
+
return;
|
|
1544
|
+
const branch = ctx.sessionManager.getBranch();
|
|
1545
|
+
const candidate = findLatestNativeCompaction(branch, model);
|
|
1546
|
+
if (!candidate || genericCompactionSelected(pi, ctx))
|
|
1547
|
+
return;
|
|
1548
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
1549
|
+
if (!auth.ok)
|
|
1550
|
+
return;
|
|
1551
|
+
const latest = findLatestNativeCompaction(branch, model, routeIdentity(model, auth));
|
|
1552
|
+
if (!latest)
|
|
1553
|
+
return;
|
|
1554
|
+
const index = branch.findIndex((entry) => entry.id === latest.entryId);
|
|
1555
|
+
const postItems = index >= 0 ? responseItemsFromEntries(model, branch.slice(index + 1)) : [];
|
|
1556
|
+
const rewritten = rewriteResponsesPayload(event.payload, latest, postItems);
|
|
1557
|
+
if (rewritten === event.payload || !isObject(rewritten))
|
|
1558
|
+
return rewritten;
|
|
1559
|
+
const systemPrompt = ctx.getSystemPrompt();
|
|
1560
|
+
return systemPrompt ? { ...rewritten, instructions: systemPrompt } : rewritten;
|
|
1561
|
+
});
|
|
1562
|
+
pi.on("session_before_compact", async (event, ctx) => {
|
|
1563
|
+
try {
|
|
1564
|
+
return await nativeAndPortableCompaction(pi, event, ctx);
|
|
1565
|
+
} catch (error) {
|
|
1566
|
+
if (event.signal.aborted || error instanceof Error && error.name === "AbortError")
|
|
1567
|
+
return;
|
|
1568
|
+
console.error(`[pi-provider-compaction] native compaction failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
export {
|
|
1574
|
+
NATIVE_COMPACTION_SUMMARY,
|
|
1575
|
+
NATIVE_COMPACTION_TYPE,
|
|
1576
|
+
NATIVE_COMPACTION_VERSION,
|
|
1577
|
+
compactOpenAIResponses,
|
|
1578
|
+
pi_provider_compaction_default as default,
|
|
1579
|
+
findLatestNativeCompaction,
|
|
1580
|
+
readNativeCompactionDetails,
|
|
1581
|
+
rewriteResponsesPayload,
|
|
1582
|
+
supportsNativeCompaction
|
|
1583
|
+
};
|