@nijaru/pi-provider-compaction 0.0.2 → 0.0.4
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/README.md +55 -41
- package/dist/index.js +1983 -0
- package/index.ts +279 -456
- package/package.json +13 -11
- package/protocol.ts +50 -12
- package/provider.ts +85 -0
- package/replay.ts +140 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1983 @@
|
|
|
1
|
+
// index.ts
|
|
2
|
+
import { createHash, createHmac, randomBytes, randomUUID } from "node:crypto";
|
|
3
|
+
import { normalizeContext as normalizeContext2 } from "@earendil-works/pi-ai";
|
|
4
|
+
import { compact, convertToLlm } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
// policy.ts
|
|
7
|
+
import {
|
|
8
|
+
CONFIG_DIR_NAME,
|
|
9
|
+
getAgentDir,
|
|
10
|
+
hasTrustRequiringProjectResources,
|
|
11
|
+
ProjectTrustStore
|
|
12
|
+
} from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { closeSync, constants as fsConstants, fstatSync, openSync, readSync } from "node:fs";
|
|
14
|
+
import { join, resolve, sep } from "node:path";
|
|
15
|
+
var COMPACTION_MODEL_FLAG = "compaction-model";
|
|
16
|
+
var PROJECT_POLICY = "compaction-policy.json";
|
|
17
|
+
var AGENT_DIR_OVERRIDE = "PI_COMPACTOR_AGENT_DIR";
|
|
18
|
+
var MAX_POLICY_BYTES = 64 * 1024;
|
|
19
|
+
var MAX_MODEL_SELECTORS = 8;
|
|
20
|
+
var MAX_SELECTOR_LENGTH = 512;
|
|
21
|
+
function normalizeSelector(value) {
|
|
22
|
+
if (typeof value !== "string")
|
|
23
|
+
return;
|
|
24
|
+
const selector = value.trim();
|
|
25
|
+
if (selector.length === 0 || selector.length > MAX_SELECTOR_LENGTH)
|
|
26
|
+
return;
|
|
27
|
+
return selector;
|
|
28
|
+
}
|
|
29
|
+
function parsePolicySelectors(value) {
|
|
30
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
31
|
+
return;
|
|
32
|
+
if (!("models" in value) || !Array.isArray(value.models))
|
|
33
|
+
return;
|
|
34
|
+
const selectors = [];
|
|
35
|
+
for (const model of value.models) {
|
|
36
|
+
const selector = normalizeSelector(model);
|
|
37
|
+
if (selector === undefined || selectors.includes(selector))
|
|
38
|
+
continue;
|
|
39
|
+
selectors.push(selector);
|
|
40
|
+
if (selectors.length === MAX_MODEL_SELECTORS)
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
return selectors;
|
|
44
|
+
}
|
|
45
|
+
function readPolicySelectors(configPath) {
|
|
46
|
+
let fd;
|
|
47
|
+
try {
|
|
48
|
+
fd = openSync(configPath, fsConstants.O_RDONLY | fsConstants.O_NONBLOCK);
|
|
49
|
+
const stat = fstatSync(fd);
|
|
50
|
+
if (!stat.isFile() || stat.size > MAX_POLICY_BYTES)
|
|
51
|
+
return;
|
|
52
|
+
const buffer = Buffer.allocUnsafe(MAX_POLICY_BYTES + 1);
|
|
53
|
+
let bytesRead = 0;
|
|
54
|
+
while (bytesRead < buffer.length) {
|
|
55
|
+
const count = readSync(fd, buffer, bytesRead, buffer.length - bytesRead, null);
|
|
56
|
+
if (count === 0)
|
|
57
|
+
break;
|
|
58
|
+
bytesRead += count;
|
|
59
|
+
}
|
|
60
|
+
if (bytesRead > MAX_POLICY_BYTES)
|
|
61
|
+
return;
|
|
62
|
+
return parsePolicySelectors(JSON.parse(buffer.subarray(0, bytesRead).toString("utf8")));
|
|
63
|
+
} catch {
|
|
64
|
+
return;
|
|
65
|
+
} finally {
|
|
66
|
+
if (fd !== undefined) {
|
|
67
|
+
try {
|
|
68
|
+
closeSync(fd);
|
|
69
|
+
} catch {}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function activeAgentDir(ctx) {
|
|
74
|
+
const override = process.env[AGENT_DIR_OVERRIDE]?.trim();
|
|
75
|
+
if (override)
|
|
76
|
+
return resolve(override);
|
|
77
|
+
try {
|
|
78
|
+
const sessionDir = ctx.sessionManager?.getSessionDir();
|
|
79
|
+
if (sessionDir) {
|
|
80
|
+
const resolvedCwd = resolve(ctx.cwd);
|
|
81
|
+
const safePath = `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
|
82
|
+
const suffix = `${sep}${join("sessions", safePath)}`;
|
|
83
|
+
const resolvedSessionDir = resolve(sessionDir);
|
|
84
|
+
if (resolvedSessionDir.endsWith(suffix)) {
|
|
85
|
+
return resolvedSessionDir.slice(0, -suffix.length);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch {}
|
|
89
|
+
return getAgentDir();
|
|
90
|
+
}
|
|
91
|
+
function isProjectPolicyTrusted(ctx, agentDir) {
|
|
92
|
+
try {
|
|
93
|
+
if (!ctx.isProjectTrusted())
|
|
94
|
+
return false;
|
|
95
|
+
return hasTrustRequiringProjectResources(ctx.cwd) || new ProjectTrustStore(agentDir).get(ctx.cwd) === true;
|
|
96
|
+
} catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function readCompactionModelFlagFromArgv(argv = process.argv.slice(2)) {
|
|
101
|
+
let value;
|
|
102
|
+
const prefix = `--${COMPACTION_MODEL_FLAG}=`;
|
|
103
|
+
for (let i = 0;i < argv.length; i++) {
|
|
104
|
+
const arg = argv[i];
|
|
105
|
+
if (arg === "--")
|
|
106
|
+
break;
|
|
107
|
+
if (arg === `--${COMPACTION_MODEL_FLAG}`) {
|
|
108
|
+
const next = argv[i + 1];
|
|
109
|
+
if (next !== undefined && !next.startsWith("-") && !next.startsWith("@")) {
|
|
110
|
+
value = next;
|
|
111
|
+
i++;
|
|
112
|
+
} else {
|
|
113
|
+
value = true;
|
|
114
|
+
}
|
|
115
|
+
} else if (arg.startsWith(prefix)) {
|
|
116
|
+
value = arg.slice(prefix.length);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
function resolveCompactionModelPolicy(pi, ctx, argv = process.argv.slice(2)) {
|
|
122
|
+
const argvRaw = readCompactionModelFlagFromArgv(argv);
|
|
123
|
+
if (typeof argvRaw === "string" && argvRaw.trim()) {
|
|
124
|
+
const selector = normalizeSelector(argvRaw);
|
|
125
|
+
return selector ? { hasSelectors: true, source: "flag" } : { hasSelectors: false };
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const rawFlag = pi.getFlag(COMPACTION_MODEL_FLAG);
|
|
129
|
+
if (typeof rawFlag === "string" && rawFlag.trim()) {
|
|
130
|
+
const selector = normalizeSelector(rawFlag);
|
|
131
|
+
return selector ? { hasSelectors: true, source: "flag" } : { hasSelectors: false };
|
|
132
|
+
}
|
|
133
|
+
} catch {}
|
|
134
|
+
const agentDir = activeAgentDir(ctx);
|
|
135
|
+
const candidates = [];
|
|
136
|
+
if (isProjectPolicyTrusted(ctx, agentDir)) {
|
|
137
|
+
candidates.push({ path: join(ctx.cwd, CONFIG_DIR_NAME, PROJECT_POLICY), source: "project-policy" });
|
|
138
|
+
}
|
|
139
|
+
candidates.push({ path: join(agentDir, PROJECT_POLICY), source: "agent-policy" });
|
|
140
|
+
for (const candidate of candidates) {
|
|
141
|
+
const selectors = readPolicySelectors(candidate.path);
|
|
142
|
+
if (selectors !== undefined) {
|
|
143
|
+
return selectors.length > 0 ? { hasSelectors: true, source: candidate.source } : { hasSelectors: false };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { hasSelectors: false };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// provider.ts
|
|
150
|
+
function optionsWithObserver(model, context, options, observe) {
|
|
151
|
+
const handler = observe(model, context, options ?? {});
|
|
152
|
+
if (!handler)
|
|
153
|
+
return options;
|
|
154
|
+
return {
|
|
155
|
+
...options,
|
|
156
|
+
onPayload: async (payload, requestModel) => {
|
|
157
|
+
const replacement = await options?.onPayload?.(payload, requestModel);
|
|
158
|
+
const prepared = replacement === undefined ? payload : replacement;
|
|
159
|
+
try {
|
|
160
|
+
return await handler(prepared);
|
|
161
|
+
} catch {
|
|
162
|
+
return prepared;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function decorateProvider(base, observe) {
|
|
168
|
+
const stream = (model, context, options) => base.stream(model, context, optionsWithObserver(model, context, options, observe));
|
|
169
|
+
const streamSimple = (model, context, options) => base.streamSimple(model, context, optionsWithObserver(model, context, options, observe));
|
|
170
|
+
const bound = new Map;
|
|
171
|
+
return new Proxy(base, {
|
|
172
|
+
get(target, key) {
|
|
173
|
+
if (key === "stream")
|
|
174
|
+
return stream;
|
|
175
|
+
if (key === "streamSimple")
|
|
176
|
+
return streamSimple;
|
|
177
|
+
const value = Reflect.get(target, key, target);
|
|
178
|
+
if (typeof value !== "function")
|
|
179
|
+
return value;
|
|
180
|
+
if (!bound.has(key))
|
|
181
|
+
bound.set(key, value.bind(target));
|
|
182
|
+
return bound.get(key);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
function installObserver(pi, ctx, observe) {
|
|
187
|
+
const model = ctx.model;
|
|
188
|
+
if (!model)
|
|
189
|
+
return;
|
|
190
|
+
const registry = ctx.modelRegistry;
|
|
191
|
+
const id = model.provider;
|
|
192
|
+
const native = registry.getRegisteredNativeProvider(id);
|
|
193
|
+
if (native) {
|
|
194
|
+
const guarded = (...args) => {
|
|
195
|
+
const handler = observe(...args);
|
|
196
|
+
return handler ? (payload) => registry.getRegisteredNativeProvider(id) === wrapper ? handler(payload) : payload : undefined;
|
|
197
|
+
};
|
|
198
|
+
const wrapper = decorateProvider(native, guarded);
|
|
199
|
+
pi.registerProvider(wrapper);
|
|
200
|
+
return () => {
|
|
201
|
+
if (registry.getRegisteredNativeProvider(id) === wrapper)
|
|
202
|
+
pi.registerProvider(native);
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const original = registry.getRegisteredProviderConfig(id);
|
|
206
|
+
if (!original?.streamSimple || original.refreshModels || original.oauth?.modifyModels)
|
|
207
|
+
return;
|
|
208
|
+
if (original.api !== model.api)
|
|
209
|
+
return;
|
|
210
|
+
const delegate = original.streamSimple.bind(original);
|
|
211
|
+
const guarded = (...args) => {
|
|
212
|
+
const handler = observe(...args);
|
|
213
|
+
return handler ? (payload) => registry.getRegisteredProviderConfig(id)?.streamSimple === wrapper ? handler(payload) : payload : undefined;
|
|
214
|
+
};
|
|
215
|
+
const wrapper = (requestModel, context, options) => delegate(requestModel, context, optionsWithObserver(requestModel, context, options, guarded));
|
|
216
|
+
pi.registerProvider(id, { ...original, api: original?.api ?? model.api, streamSimple: wrapper });
|
|
217
|
+
return () => {
|
|
218
|
+
const current = registry.getRegisteredProviderConfig(id);
|
|
219
|
+
if (current?.streamSimple !== wrapper || registry.getRegisteredNativeProvider(id))
|
|
220
|
+
return;
|
|
221
|
+
const restored = { ...current };
|
|
222
|
+
if (original?.streamSimple)
|
|
223
|
+
restored.streamSimple = original.streamSimple;
|
|
224
|
+
else
|
|
225
|
+
delete restored.streamSimple;
|
|
226
|
+
if (!original?.api && restored.api === model.api)
|
|
227
|
+
delete restored.api;
|
|
228
|
+
pi.unregisterProvider(id);
|
|
229
|
+
if (Object.keys(restored).length)
|
|
230
|
+
pi.registerProvider(id, restored);
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// replay.ts
|
|
235
|
+
import { getCurrentSystemPrompt, getCurrentTools as getCurrentTools2, getDeclaredTools, normalizeContext, resolveTranscript as resolveTranscript2 } from "@earendil-works/pi-ai";
|
|
236
|
+
|
|
237
|
+
// node_modules/@earendil-works/pi-ai/dist/api/constrained-sampling.js
|
|
238
|
+
class UnsupportedStrictJsonSchemaError extends Error {
|
|
239
|
+
}
|
|
240
|
+
var UNSUPPORTED_STRICT_SCHEMA_KEYS = [
|
|
241
|
+
"$ref",
|
|
242
|
+
"$defs",
|
|
243
|
+
"definitions",
|
|
244
|
+
"allOf",
|
|
245
|
+
"oneOf",
|
|
246
|
+
"patternProperties",
|
|
247
|
+
"dependentSchemas",
|
|
248
|
+
"dependencies",
|
|
249
|
+
"unevaluatedProperties",
|
|
250
|
+
"propertyNames",
|
|
251
|
+
"contains",
|
|
252
|
+
"prefixItems",
|
|
253
|
+
"not",
|
|
254
|
+
"if",
|
|
255
|
+
"then",
|
|
256
|
+
"else"
|
|
257
|
+
];
|
|
258
|
+
function isJsonSchemaObject(value) {
|
|
259
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
260
|
+
}
|
|
261
|
+
function isStructuredSchema(schema) {
|
|
262
|
+
if (!isJsonSchemaObject(schema))
|
|
263
|
+
return false;
|
|
264
|
+
const types = typeof schema.type === "string" ? [schema.type] : Array.isArray(schema.type) ? schema.type : [];
|
|
265
|
+
return types.includes("object") || types.includes("array") || schema.properties !== undefined || schema.items !== undefined;
|
|
266
|
+
}
|
|
267
|
+
function schemaAllowsNull(schema) {
|
|
268
|
+
if (!isJsonSchemaObject(schema))
|
|
269
|
+
return false;
|
|
270
|
+
if (schema.type === "null" || Array.isArray(schema.type) && schema.type.includes("null"))
|
|
271
|
+
return true;
|
|
272
|
+
if (schema.const === null || Array.isArray(schema.enum) && schema.enum.includes(null))
|
|
273
|
+
return true;
|
|
274
|
+
return Array.isArray(schema.anyOf) && schema.anyOf.some((variant) => schemaAllowsNull(variant));
|
|
275
|
+
}
|
|
276
|
+
function makeJsonSchemaNodeStrict(schema) {
|
|
277
|
+
if (!isJsonSchemaObject(schema)) {
|
|
278
|
+
throw new UnsupportedStrictJsonSchemaError("boolean schemas are unsupported");
|
|
279
|
+
}
|
|
280
|
+
for (const key of UNSUPPORTED_STRICT_SCHEMA_KEYS) {
|
|
281
|
+
if (schema[key] !== undefined) {
|
|
282
|
+
throw new UnsupportedStrictJsonSchemaError(`${key} schemas are unsupported`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (schema.anyOf !== undefined) {
|
|
286
|
+
if (!Array.isArray(schema.anyOf) || schema.anyOf.length === 0) {
|
|
287
|
+
throw new UnsupportedStrictJsonSchemaError("anyOf must contain at least one schema");
|
|
288
|
+
}
|
|
289
|
+
for (const variant of schema.anyOf) {
|
|
290
|
+
if (isStructuredSchema(variant)) {
|
|
291
|
+
throw new UnsupportedStrictJsonSchemaError("object and array unions are unsupported");
|
|
292
|
+
}
|
|
293
|
+
makeJsonSchemaNodeStrict(variant);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (schema.items !== undefined) {
|
|
297
|
+
if (Array.isArray(schema.items)) {
|
|
298
|
+
throw new UnsupportedStrictJsonSchemaError("tuple schemas are unsupported");
|
|
299
|
+
}
|
|
300
|
+
makeJsonSchemaNodeStrict(schema.items);
|
|
301
|
+
}
|
|
302
|
+
const isObjectSchema = schema.type === "object";
|
|
303
|
+
if (schema.properties !== undefined && !isObjectSchema) {
|
|
304
|
+
throw new UnsupportedStrictJsonSchemaError("properties require type object");
|
|
305
|
+
}
|
|
306
|
+
if (!isObjectSchema)
|
|
307
|
+
return;
|
|
308
|
+
if (schema.additionalProperties !== undefined && schema.additionalProperties !== false) {
|
|
309
|
+
throw new UnsupportedStrictJsonSchemaError("schema-valued or true additionalProperties is unsupported");
|
|
310
|
+
}
|
|
311
|
+
if (schema.properties !== undefined && !isJsonSchemaObject(schema.properties)) {
|
|
312
|
+
throw new UnsupportedStrictJsonSchemaError("object properties must be a schema map");
|
|
313
|
+
}
|
|
314
|
+
if (schema.required !== undefined && (!Array.isArray(schema.required) || schema.required.some((key) => typeof key !== "string"))) {
|
|
315
|
+
throw new UnsupportedStrictJsonSchemaError("object required must be a string array");
|
|
316
|
+
}
|
|
317
|
+
const properties = schema.properties ?? {};
|
|
318
|
+
const propertyNames = Object.keys(properties);
|
|
319
|
+
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
320
|
+
if ([...required].some((key) => !propertyNames.includes(key))) {
|
|
321
|
+
throw new UnsupportedStrictJsonSchemaError("required contains an unknown property");
|
|
322
|
+
}
|
|
323
|
+
for (const [key, property] of Object.entries(properties)) {
|
|
324
|
+
makeJsonSchemaNodeStrict(property);
|
|
325
|
+
if (!required.has(key) && !schemaAllowsNull(property)) {
|
|
326
|
+
properties[key] = { anyOf: [property, { type: "null" }] };
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
schema.required = propertyNames;
|
|
330
|
+
schema.additionalProperties = false;
|
|
331
|
+
}
|
|
332
|
+
function makeStrictJsonSchema(schema) {
|
|
333
|
+
const cloned = structuredClone(schema);
|
|
334
|
+
if (!isJsonSchemaObject(cloned)) {
|
|
335
|
+
throw new UnsupportedStrictJsonSchemaError("root schema must have type object");
|
|
336
|
+
}
|
|
337
|
+
makeJsonSchemaNodeStrict(cloned);
|
|
338
|
+
if (cloned.type !== "object") {
|
|
339
|
+
throw new UnsupportedStrictJsonSchemaError("root schema must have type object");
|
|
340
|
+
}
|
|
341
|
+
return cloned;
|
|
342
|
+
}
|
|
343
|
+
function getJsonSchemaToolParameters(tool, strict) {
|
|
344
|
+
return strict === true ? makeStrictJsonSchema(tool.parameters) : tool.parameters;
|
|
345
|
+
}
|
|
346
|
+
function getGrammarToolInput(toolName, arguments_, inputProperty) {
|
|
347
|
+
const input = arguments_[inputProperty];
|
|
348
|
+
if (typeof input !== "string") {
|
|
349
|
+
throw new Error(`Grammar tool call "${toolName}" requires argument "${inputProperty}" to be a string.`);
|
|
350
|
+
}
|
|
351
|
+
return input;
|
|
352
|
+
}
|
|
353
|
+
function inferGrammarInputProperty(tool) {
|
|
354
|
+
const schema = tool.parameters;
|
|
355
|
+
if (schema.type !== "object") {
|
|
356
|
+
throw new Error("grammar constrained sampling requires an object parameter schema");
|
|
357
|
+
}
|
|
358
|
+
if (!Array.isArray(schema.required) || schema.required.length !== 1 || typeof schema.required[0] !== "string") {
|
|
359
|
+
throw new Error("grammar constrained sampling requires exactly one required string property");
|
|
360
|
+
}
|
|
361
|
+
const inputProperty = schema.required[0];
|
|
362
|
+
if (!schema.properties?.[inputProperty]) {
|
|
363
|
+
throw new Error(`grammar constrained sampling requires a properties entry for ${inputProperty}`);
|
|
364
|
+
}
|
|
365
|
+
if (schema.properties[inputProperty]?.type !== "string") {
|
|
366
|
+
throw new Error(`grammar constrained sampling property ${inputProperty} must have type string`);
|
|
367
|
+
}
|
|
368
|
+
return inputProperty;
|
|
369
|
+
}
|
|
370
|
+
function resolveJsonSchemaStrictSampling(tool, supportsStrictMode) {
|
|
371
|
+
const config = tool.constrainedSampling;
|
|
372
|
+
if (!config || config.type !== "json_schema")
|
|
373
|
+
return;
|
|
374
|
+
if (supportsStrictMode) {
|
|
375
|
+
try {
|
|
376
|
+
makeStrictJsonSchema(tool.parameters);
|
|
377
|
+
return true;
|
|
378
|
+
} catch (error) {
|
|
379
|
+
if (!(error instanceof UnsupportedStrictJsonSchemaError))
|
|
380
|
+
throw error;
|
|
381
|
+
if (config.strict !== "require")
|
|
382
|
+
return;
|
|
383
|
+
throw new Error(`Tool "${tool.name}" requires JSON-schema constrained sampling, but ${error.message}.`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (config.strict === "require") {
|
|
387
|
+
throw new Error(`Tool "${tool.name}" requires JSON-schema constrained sampling, but strict tools are unsupported.`);
|
|
388
|
+
}
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
function resolveGrammarConstrainedSampling(tool, supportsOpenAIGrammarTools) {
|
|
392
|
+
const config = tool.constrainedSampling;
|
|
393
|
+
if (!config || config.type !== "grammar") {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (!supportsOpenAIGrammarTools) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const larkDefinition = config.variants.openai_lark;
|
|
400
|
+
const regexDefinition = config.variants.openai_regex;
|
|
401
|
+
const hasLarkDefinition = typeof larkDefinition === "string" && larkDefinition.trim().length > 0;
|
|
402
|
+
const hasRegexDefinition = typeof regexDefinition === "string" && regexDefinition.trim().length > 0;
|
|
403
|
+
if (!hasLarkDefinition && !hasRegexDefinition) {
|
|
404
|
+
throw new Error(`Tool "${tool.name}" cannot use grammar constrained sampling: no supported grammar variant was provided.`);
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
return {
|
|
408
|
+
format: hasLarkDefinition ? "lark" : "regex",
|
|
409
|
+
definition: hasLarkDefinition ? larkDefinition : regexDefinition,
|
|
410
|
+
inputProperty: inferGrammarInputProperty(tool)
|
|
411
|
+
};
|
|
412
|
+
} catch (error) {
|
|
413
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
414
|
+
throw new Error(`Tool "${tool.name}" cannot use grammar constrained sampling: ${message}.`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
function createGrammarToolInputProperties(tools, supportsOpenAIGrammarTools) {
|
|
418
|
+
const properties = new Map;
|
|
419
|
+
for (const tool of tools ?? []) {
|
|
420
|
+
const grammar = resolveGrammarConstrainedSampling(tool, supportsOpenAIGrammarTools);
|
|
421
|
+
if (grammar) {
|
|
422
|
+
properties.set(tool.name, grammar.inputProperty);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return properties;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// node_modules/@earendil-works/pi-ai/dist/utils/text.js
|
|
429
|
+
function contentText(content, separator = `
|
|
430
|
+
`) {
|
|
431
|
+
if (typeof content === "string")
|
|
432
|
+
return content;
|
|
433
|
+
return content.filter((block) => block.type === "text").map((block) => block.text).join(separator);
|
|
434
|
+
}
|
|
435
|
+
function getSystemMessageText(message) {
|
|
436
|
+
const parts = [contentText(message.content)];
|
|
437
|
+
for (const text of Object.values(message.sections ?? {})) {
|
|
438
|
+
if (text !== null)
|
|
439
|
+
parts.push(text);
|
|
440
|
+
}
|
|
441
|
+
return parts.filter((part) => part.length > 0).join(`
|
|
442
|
+
|
|
443
|
+
`);
|
|
444
|
+
}
|
|
445
|
+
function renderSystemMessageUpdate(message) {
|
|
446
|
+
const parts = [];
|
|
447
|
+
const text = contentText(message.content);
|
|
448
|
+
if (text.length > 0)
|
|
449
|
+
parts.push(text);
|
|
450
|
+
for (const [name, value] of Object.entries(message.sections ?? {})) {
|
|
451
|
+
parts.push(value === null ? `Removed system prompt section "${name}".` : `Updated system prompt section "${name}":
|
|
452
|
+
|
|
453
|
+
${value}`);
|
|
454
|
+
}
|
|
455
|
+
return parts.join(`
|
|
456
|
+
|
|
457
|
+
`);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// node_modules/@earendil-works/pi-ai/dist/utils/transcript.js
|
|
461
|
+
function isSystemMessage(message) {
|
|
462
|
+
return message.role === "system";
|
|
463
|
+
}
|
|
464
|
+
function getInitialSystemMessage(messages) {
|
|
465
|
+
const first = messages[0];
|
|
466
|
+
return first && isSystemMessage(first) ? first : undefined;
|
|
467
|
+
}
|
|
468
|
+
function getCurrentTools(messages) {
|
|
469
|
+
const tools = new Map;
|
|
470
|
+
for (const message of messages) {
|
|
471
|
+
if (!isSystemMessage(message))
|
|
472
|
+
continue;
|
|
473
|
+
for (const tool of message.toolsRemoved ?? [])
|
|
474
|
+
tools.delete(tool.name);
|
|
475
|
+
for (const tool of message.toolsAdded ?? [])
|
|
476
|
+
tools.set(tool.name, tool);
|
|
477
|
+
}
|
|
478
|
+
return [...tools.values()];
|
|
479
|
+
}
|
|
480
|
+
function getCurrentSystemMessage(messages) {
|
|
481
|
+
const content = [];
|
|
482
|
+
const sections = new Map;
|
|
483
|
+
let timestamp;
|
|
484
|
+
for (const message of messages) {
|
|
485
|
+
if (!isSystemMessage(message))
|
|
486
|
+
continue;
|
|
487
|
+
timestamp ??= message.timestamp;
|
|
488
|
+
const text = contentText(message.content);
|
|
489
|
+
if (text.length > 0)
|
|
490
|
+
content.push(text);
|
|
491
|
+
for (const [name, value] of Object.entries(message.sections ?? {})) {
|
|
492
|
+
if (value === null)
|
|
493
|
+
sections.delete(name);
|
|
494
|
+
else
|
|
495
|
+
sections.set(name, value);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
const tools = getCurrentTools(messages);
|
|
499
|
+
if (timestamp === undefined && tools.length === 0)
|
|
500
|
+
return;
|
|
501
|
+
return {
|
|
502
|
+
role: "system",
|
|
503
|
+
content: content.join(`
|
|
504
|
+
|
|
505
|
+
`),
|
|
506
|
+
...sections.size > 0 ? { sections: Object.fromEntries(sections) } : {},
|
|
507
|
+
...tools.length > 0 ? { toolsAdded: tools } : {},
|
|
508
|
+
timestamp: timestamp ?? 0
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
function collapseSystemMessages(context) {
|
|
512
|
+
const head = getCurrentSystemMessage(context.messages);
|
|
513
|
+
const messages = context.messages.filter((message) => message.role !== "system");
|
|
514
|
+
return { messages: head ? [head, ...messages] : messages };
|
|
515
|
+
}
|
|
516
|
+
function resolveTranscript(context, supportsMidConvoSystemMessages) {
|
|
517
|
+
return supportsMidConvoSystemMessages ? context : collapseSystemMessages(context);
|
|
518
|
+
}
|
|
519
|
+
function hasNonAdditiveToolChanges(messages) {
|
|
520
|
+
const declared = new Set;
|
|
521
|
+
for (const message of messages) {
|
|
522
|
+
if (!isSystemMessage(message))
|
|
523
|
+
continue;
|
|
524
|
+
if ((message.toolsRemoved?.length ?? 0) > 0)
|
|
525
|
+
return true;
|
|
526
|
+
for (const tool of message.toolsAdded ?? []) {
|
|
527
|
+
if (declared.has(tool.name))
|
|
528
|
+
return true;
|
|
529
|
+
declared.add(tool.name);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
function resolveTranscriptTools(messages, supportsToolAdditions) {
|
|
535
|
+
const anchorsAdditions = supportsToolAdditions && !hasNonAdditiveToolChanges(messages);
|
|
536
|
+
return {
|
|
537
|
+
requestTools: anchorsAdditions ? getInitialSystemMessage(messages)?.toolsAdded ?? [] : getCurrentTools(messages),
|
|
538
|
+
anchorsAdditions
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// node_modules/@earendil-works/pi-ai/dist/utils/hash.js
|
|
543
|
+
function shortHash(str) {
|
|
544
|
+
let h1 = 3735928559;
|
|
545
|
+
let h2 = 1103547991;
|
|
546
|
+
for (let i = 0;i < str.length; i++) {
|
|
547
|
+
const ch = str.charCodeAt(i);
|
|
548
|
+
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
549
|
+
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
550
|
+
}
|
|
551
|
+
h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
|
|
552
|
+
h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
|
|
553
|
+
return (h2 >>> 0).toString(36) + (h1 >>> 0).toString(36);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// node_modules/@earendil-works/pi-ai/dist/utils/sanitize-unicode.js
|
|
557
|
+
function sanitizeSurrogates(text) {
|
|
558
|
+
return text.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "");
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// node_modules/@earendil-works/pi-ai/dist/api/transform-messages.js
|
|
562
|
+
var NON_VISION_USER_IMAGE_PLACEHOLDER = "(image omitted: model does not support images)";
|
|
563
|
+
var NON_VISION_TOOL_IMAGE_PLACEHOLDER = "(tool image omitted: model does not support images)";
|
|
564
|
+
function replaceImagesWithPlaceholder(content, placeholder) {
|
|
565
|
+
const result = [];
|
|
566
|
+
let previousWasPlaceholder = false;
|
|
567
|
+
for (const block of content) {
|
|
568
|
+
if (block.type === "image") {
|
|
569
|
+
if (!previousWasPlaceholder) {
|
|
570
|
+
result.push({ type: "text", text: placeholder });
|
|
571
|
+
}
|
|
572
|
+
previousWasPlaceholder = true;
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
result.push(block);
|
|
576
|
+
previousWasPlaceholder = block.text === placeholder;
|
|
577
|
+
}
|
|
578
|
+
return result;
|
|
579
|
+
}
|
|
580
|
+
function downgradeUnsupportedImages(messages, model) {
|
|
581
|
+
if (model.input.includes("image")) {
|
|
582
|
+
return messages;
|
|
583
|
+
}
|
|
584
|
+
return messages.map((msg) => {
|
|
585
|
+
if (msg.role === "user" && Array.isArray(msg.content)) {
|
|
586
|
+
return {
|
|
587
|
+
...msg,
|
|
588
|
+
content: replaceImagesWithPlaceholder(msg.content, NON_VISION_USER_IMAGE_PLACEHOLDER)
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
if (msg.role === "toolResult") {
|
|
592
|
+
return {
|
|
593
|
+
...msg,
|
|
594
|
+
content: replaceImagesWithPlaceholder(msg.content, NON_VISION_TOOL_IMAGE_PLACEHOLDER)
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
return msg;
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
function transformMessages(messages, model, normalizeToolCallId) {
|
|
601
|
+
const toolCallIdMap = new Map;
|
|
602
|
+
const normalizedMessages = messages.map((msg) => msg.content == null ? { ...msg, content: [] } : msg);
|
|
603
|
+
const imageAwareMessages = downgradeUnsupportedImages(normalizedMessages, model);
|
|
604
|
+
const transformed = imageAwareMessages.map((msg) => {
|
|
605
|
+
if (msg.role === "system" || msg.role === "user") {
|
|
606
|
+
return msg;
|
|
607
|
+
}
|
|
608
|
+
if (msg.role === "toolResult") {
|
|
609
|
+
const normalizedId = toolCallIdMap.get(msg.toolCallId);
|
|
610
|
+
if (normalizedId && normalizedId !== msg.toolCallId) {
|
|
611
|
+
return { ...msg, toolCallId: normalizedId };
|
|
612
|
+
}
|
|
613
|
+
return msg;
|
|
614
|
+
}
|
|
615
|
+
if (msg.role === "assistant") {
|
|
616
|
+
const assistantMsg = msg;
|
|
617
|
+
const isSameModel = assistantMsg.provider === model.provider && assistantMsg.api === model.api && assistantMsg.model === model.id;
|
|
618
|
+
const transformedContent = assistantMsg.content.flatMap((block) => {
|
|
619
|
+
if (block.type === "thinking") {
|
|
620
|
+
if (block.redacted) {
|
|
621
|
+
return isSameModel ? block : [];
|
|
622
|
+
}
|
|
623
|
+
if (isSameModel && block.thinkingSignature)
|
|
624
|
+
return block;
|
|
625
|
+
if (!block.thinking || block.thinking.trim() === "")
|
|
626
|
+
return [];
|
|
627
|
+
if (isSameModel)
|
|
628
|
+
return block;
|
|
629
|
+
return {
|
|
630
|
+
type: "text",
|
|
631
|
+
text: block.thinking
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
if (block.type === "text") {
|
|
635
|
+
if (isSameModel)
|
|
636
|
+
return block;
|
|
637
|
+
return {
|
|
638
|
+
type: "text",
|
|
639
|
+
text: block.text
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
if (block.type === "toolCall") {
|
|
643
|
+
const toolCall = block;
|
|
644
|
+
let normalizedToolCall = toolCall;
|
|
645
|
+
if (!isSameModel && toolCall.thoughtSignature) {
|
|
646
|
+
normalizedToolCall = { ...toolCall };
|
|
647
|
+
delete normalizedToolCall.thoughtSignature;
|
|
648
|
+
}
|
|
649
|
+
if (!isSameModel && normalizeToolCallId) {
|
|
650
|
+
const normalizedId = normalizeToolCallId(toolCall.id, model, assistantMsg);
|
|
651
|
+
if (normalizedId !== toolCall.id) {
|
|
652
|
+
toolCallIdMap.set(toolCall.id, normalizedId);
|
|
653
|
+
normalizedToolCall = { ...normalizedToolCall, id: normalizedId };
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return normalizedToolCall;
|
|
657
|
+
}
|
|
658
|
+
return block;
|
|
659
|
+
});
|
|
660
|
+
return {
|
|
661
|
+
...assistantMsg,
|
|
662
|
+
content: transformedContent
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
return msg;
|
|
666
|
+
});
|
|
667
|
+
const result = [];
|
|
668
|
+
let pendingToolCalls = [];
|
|
669
|
+
let existingToolResultIds = new Set;
|
|
670
|
+
const heldSystemMessages = [];
|
|
671
|
+
const closePendingToolCalls = () => {
|
|
672
|
+
if (pendingToolCalls.length > 0) {
|
|
673
|
+
for (const tc of pendingToolCalls) {
|
|
674
|
+
if (!existingToolResultIds.has(tc.id)) {
|
|
675
|
+
result.push({
|
|
676
|
+
role: "toolResult",
|
|
677
|
+
toolCallId: tc.id,
|
|
678
|
+
toolName: tc.name,
|
|
679
|
+
content: [{ type: "text", text: "No result provided" }],
|
|
680
|
+
isError: true,
|
|
681
|
+
timestamp: Date.now()
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
pendingToolCalls = [];
|
|
686
|
+
existingToolResultIds = new Set;
|
|
687
|
+
}
|
|
688
|
+
result.push(...heldSystemMessages);
|
|
689
|
+
heldSystemMessages.length = 0;
|
|
690
|
+
};
|
|
691
|
+
for (let i = 0;i < transformed.length; i++) {
|
|
692
|
+
const msg = transformed[i];
|
|
693
|
+
if (msg.role === "assistant") {
|
|
694
|
+
closePendingToolCalls();
|
|
695
|
+
const assistantMsg = msg;
|
|
696
|
+
if (assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted") {
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
const toolCalls = assistantMsg.content.filter((b) => b.type === "toolCall");
|
|
700
|
+
if (toolCalls.length > 0) {
|
|
701
|
+
pendingToolCalls = toolCalls;
|
|
702
|
+
existingToolResultIds = new Set;
|
|
703
|
+
}
|
|
704
|
+
result.push(msg);
|
|
705
|
+
} else if (msg.role === "toolResult") {
|
|
706
|
+
existingToolResultIds.add(msg.toolCallId);
|
|
707
|
+
result.push(msg);
|
|
708
|
+
} else if (msg.role === "system") {
|
|
709
|
+
if (pendingToolCalls.length > 0) {
|
|
710
|
+
heldSystemMessages.push(msg);
|
|
711
|
+
} else {
|
|
712
|
+
result.push(msg);
|
|
713
|
+
}
|
|
714
|
+
} else if (msg.role === "user") {
|
|
715
|
+
closePendingToolCalls();
|
|
716
|
+
result.push(msg);
|
|
717
|
+
} else {
|
|
718
|
+
result.push(msg);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
closePendingToolCalls();
|
|
722
|
+
return result;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// node_modules/@earendil-works/pi-ai/dist/api/openai-responses-shared.js
|
|
726
|
+
function parseTextSignature(signature) {
|
|
727
|
+
if (!signature)
|
|
728
|
+
return;
|
|
729
|
+
if (signature.startsWith("{")) {
|
|
730
|
+
try {
|
|
731
|
+
const parsed = JSON.parse(signature);
|
|
732
|
+
if (parsed.v === 1 && typeof parsed.id === "string") {
|
|
733
|
+
if (parsed.phase === "commentary" || parsed.phase === "final_answer") {
|
|
734
|
+
return { id: parsed.id, phase: parsed.phase };
|
|
735
|
+
}
|
|
736
|
+
return { id: parsed.id };
|
|
737
|
+
}
|
|
738
|
+
} catch {}
|
|
739
|
+
}
|
|
740
|
+
return { id: signature };
|
|
741
|
+
}
|
|
742
|
+
function convertToolResultOutput(model, content) {
|
|
743
|
+
const textResult = content.filter((c) => c.type === "text").map((c) => c.text).join(`
|
|
744
|
+
`);
|
|
745
|
+
const images = content.filter((c) => c.type === "image");
|
|
746
|
+
const hasText = textResult.length > 0;
|
|
747
|
+
if (images.length === 0 || !model.input.includes("image")) {
|
|
748
|
+
return sanitizeSurrogates(hasText ? textResult : images.length > 0 ? "(see attached image)" : "(no tool output)");
|
|
749
|
+
}
|
|
750
|
+
const output = [];
|
|
751
|
+
if (hasText) {
|
|
752
|
+
output.push({ type: "input_text", text: sanitizeSurrogates(textResult) });
|
|
753
|
+
}
|
|
754
|
+
for (const image of images) {
|
|
755
|
+
output.push({
|
|
756
|
+
type: "input_image",
|
|
757
|
+
detail: "auto",
|
|
758
|
+
image_url: `data:${image.mimeType};base64,${image.data}`
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
return output;
|
|
762
|
+
}
|
|
763
|
+
function convertResponsesMessages(model, context, allowedToolCallProviders, options) {
|
|
764
|
+
const normalizedContext = resolveTranscript(context, options?.supportsMidConvoSystemMessages);
|
|
765
|
+
const messages = [];
|
|
766
|
+
const normalizeIdPart = (part) => {
|
|
767
|
+
const sanitized = part.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
768
|
+
const normalized = sanitized.length > 64 ? sanitized.slice(0, 64) : sanitized;
|
|
769
|
+
return normalized.replace(/_+$/, "");
|
|
770
|
+
};
|
|
771
|
+
const buildForeignResponsesItemId = (itemId) => {
|
|
772
|
+
const normalized = `fc_${shortHash(itemId)}`;
|
|
773
|
+
return normalized.length > 64 ? normalized.slice(0, 64) : normalized;
|
|
774
|
+
};
|
|
775
|
+
const normalizeToolCallId = (id, _targetModel, source) => {
|
|
776
|
+
if (!allowedToolCallProviders.has(model.provider))
|
|
777
|
+
return normalizeIdPart(id);
|
|
778
|
+
if (!id.includes("|"))
|
|
779
|
+
return normalizeIdPart(id);
|
|
780
|
+
const [callId, itemId] = id.split("|");
|
|
781
|
+
const normalizedCallId = normalizeIdPart(callId);
|
|
782
|
+
const isForeignToolCall = source.provider !== model.provider || source.api !== model.api;
|
|
783
|
+
let normalizedItemId = isForeignToolCall ? buildForeignResponsesItemId(itemId) : normalizeIdPart(itemId);
|
|
784
|
+
if (!normalizedItemId.startsWith("fc_")) {
|
|
785
|
+
normalizedItemId = normalizeIdPart(`fc_${normalizedItemId}`);
|
|
786
|
+
}
|
|
787
|
+
return `${normalizedCallId}|${normalizedItemId}`;
|
|
788
|
+
};
|
|
789
|
+
const transformedMessages = transformMessages(normalizedContext.messages, model, normalizeToolCallId);
|
|
790
|
+
const transcriptTools = resolveTranscriptTools(normalizedContext.messages, (options?.supportsAdditionalTools ?? false) || (options?.supportsToolSearch ?? false));
|
|
791
|
+
const appendSystemToolAdditions = (message, seed) => {
|
|
792
|
+
const tools = transcriptTools.anchorsAdditions ? message.toolsAdded ?? [] : [];
|
|
793
|
+
if (tools.length === 0)
|
|
794
|
+
return;
|
|
795
|
+
if (options?.supportsAdditionalTools) {
|
|
796
|
+
messages.push({
|
|
797
|
+
type: "additional_tools",
|
|
798
|
+
role: "developer",
|
|
799
|
+
tools: convertResponsesTools(tools, options.toolOptions)
|
|
800
|
+
});
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
if (!options?.supportsToolSearch)
|
|
804
|
+
return;
|
|
805
|
+
const names = tools.map((tool) => tool.name);
|
|
806
|
+
const callId = `pi_tool_load_${shortHash(`${seed}:${names.join(",")}`)}`;
|
|
807
|
+
messages.push({
|
|
808
|
+
type: "tool_search_call",
|
|
809
|
+
call_id: callId,
|
|
810
|
+
execution: "client",
|
|
811
|
+
status: "completed",
|
|
812
|
+
arguments: { query: names.join(" "), limit: names.length }
|
|
813
|
+
});
|
|
814
|
+
messages.push({
|
|
815
|
+
type: "tool_search_output",
|
|
816
|
+
call_id: callId,
|
|
817
|
+
execution: "client",
|
|
818
|
+
status: "completed",
|
|
819
|
+
tools: convertResponsesTools(tools, { ...options.toolOptions, toolSearchResult: true })
|
|
820
|
+
});
|
|
821
|
+
};
|
|
822
|
+
const includeInitialSystemMessage = options?.includeSystemPrompt ?? true;
|
|
823
|
+
const compat = model.compat;
|
|
824
|
+
const instructionRole = model.reasoning && compat?.supportsDeveloperRole !== false ? "developer" : "system";
|
|
825
|
+
let msgIndex = 0;
|
|
826
|
+
let sourceIndex = 0;
|
|
827
|
+
for (const msg of transformedMessages) {
|
|
828
|
+
const isLeadingSystemMessage = sourceIndex++ === 0 && msg.role === "system";
|
|
829
|
+
if (msg.role === "system") {
|
|
830
|
+
if (!isLeadingSystemMessage)
|
|
831
|
+
appendSystemToolAdditions(msg, `system:${msgIndex}`);
|
|
832
|
+
if (!isLeadingSystemMessage || includeInitialSystemMessage) {
|
|
833
|
+
const text = isLeadingSystemMessage ? getSystemMessageText(msg) : renderSystemMessageUpdate(msg);
|
|
834
|
+
if (text.length > 0) {
|
|
835
|
+
messages.push({ role: instructionRole, content: sanitizeSurrogates(text) });
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
} else if (msg.role === "user") {
|
|
839
|
+
if (typeof msg.content === "string") {
|
|
840
|
+
messages.push({
|
|
841
|
+
role: "user",
|
|
842
|
+
content: [{ type: "input_text", text: sanitizeSurrogates(msg.content) }]
|
|
843
|
+
});
|
|
844
|
+
} else {
|
|
845
|
+
const content = msg.content.map((item) => {
|
|
846
|
+
if (item.type === "text") {
|
|
847
|
+
return {
|
|
848
|
+
type: "input_text",
|
|
849
|
+
text: sanitizeSurrogates(item.text)
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
return {
|
|
853
|
+
type: "input_image",
|
|
854
|
+
detail: "auto",
|
|
855
|
+
image_url: `data:${item.mimeType};base64,${item.data}`
|
|
856
|
+
};
|
|
857
|
+
});
|
|
858
|
+
if (content.length === 0)
|
|
859
|
+
continue;
|
|
860
|
+
messages.push({
|
|
861
|
+
role: "user",
|
|
862
|
+
content
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
} else if (msg.role === "assistant") {
|
|
866
|
+
const output = [];
|
|
867
|
+
const assistantMsg = msg;
|
|
868
|
+
const isSameProviderAndApi = assistantMsg.provider === model.provider && assistantMsg.api === model.api;
|
|
869
|
+
const isSameModel = isSameProviderAndApi && assistantMsg.model === model.id;
|
|
870
|
+
const isDifferentModel = isSameProviderAndApi && assistantMsg.model !== model.id;
|
|
871
|
+
let textBlockIndex = 0;
|
|
872
|
+
for (const block of msg.content) {
|
|
873
|
+
if (block.type === "thinking") {
|
|
874
|
+
if (block.thinkingSignature) {
|
|
875
|
+
const reasoningItem = JSON.parse(block.thinkingSignature);
|
|
876
|
+
output.push(reasoningItem);
|
|
877
|
+
}
|
|
878
|
+
} else if (block.type === "text") {
|
|
879
|
+
const textBlock = block;
|
|
880
|
+
const parsedSignature = parseTextSignature(textBlock.textSignature);
|
|
881
|
+
const fallbackMessageId = textBlockIndex === 0 ? `msg_pi_${msgIndex}` : `msg_pi_${msgIndex}_${textBlockIndex}`;
|
|
882
|
+
textBlockIndex++;
|
|
883
|
+
let msgId = parsedSignature?.id;
|
|
884
|
+
if (!msgId) {
|
|
885
|
+
msgId = fallbackMessageId;
|
|
886
|
+
} else if (msgId.length > 64) {
|
|
887
|
+
msgId = `msg_${shortHash(msgId)}`;
|
|
888
|
+
}
|
|
889
|
+
output.push({
|
|
890
|
+
type: "message",
|
|
891
|
+
role: "assistant",
|
|
892
|
+
content: [{ type: "output_text", text: sanitizeSurrogates(textBlock.text), annotations: [] }],
|
|
893
|
+
status: "completed",
|
|
894
|
+
id: msgId,
|
|
895
|
+
phase: parsedSignature?.phase
|
|
896
|
+
});
|
|
897
|
+
} else if (block.type === "toolCall") {
|
|
898
|
+
const toolCall = block;
|
|
899
|
+
const [callId, itemIdRaw] = toolCall.id.split("|");
|
|
900
|
+
const customInputProperty = options?.grammarToolInputProperties?.get(toolCall.name);
|
|
901
|
+
let itemId = itemIdRaw;
|
|
902
|
+
if (isDifferentModel && itemId?.startsWith("fc_") || customInputProperty === undefined && !itemId?.startsWith("fc_")) {
|
|
903
|
+
itemId = undefined;
|
|
904
|
+
}
|
|
905
|
+
if (customInputProperty !== undefined) {
|
|
906
|
+
output.push({
|
|
907
|
+
type: "custom_tool_call",
|
|
908
|
+
id: itemId,
|
|
909
|
+
call_id: callId,
|
|
910
|
+
name: toolCall.name,
|
|
911
|
+
input: sanitizeSurrogates(getGrammarToolInput(toolCall.name, toolCall.arguments, customInputProperty)),
|
|
912
|
+
...isSameModel && toolCall.namespace !== undefined ? { namespace: toolCall.namespace } : {}
|
|
913
|
+
});
|
|
914
|
+
} else {
|
|
915
|
+
output.push({
|
|
916
|
+
type: "function_call",
|
|
917
|
+
id: itemId,
|
|
918
|
+
call_id: callId,
|
|
919
|
+
name: toolCall.name,
|
|
920
|
+
arguments: JSON.stringify(toolCall.arguments),
|
|
921
|
+
...isSameModel && toolCall.namespace !== undefined ? { namespace: toolCall.namespace } : {}
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
if (output.length === 0)
|
|
927
|
+
continue;
|
|
928
|
+
messages.push(...output);
|
|
929
|
+
} else if (msg.role === "toolResult") {
|
|
930
|
+
const [callId] = msg.toolCallId.split("|");
|
|
931
|
+
const output = convertToolResultOutput(model, msg.content);
|
|
932
|
+
if (options?.grammarToolInputProperties?.has(msg.toolName)) {
|
|
933
|
+
messages.push({
|
|
934
|
+
type: "custom_tool_call_output",
|
|
935
|
+
call_id: callId,
|
|
936
|
+
output
|
|
937
|
+
});
|
|
938
|
+
} else {
|
|
939
|
+
messages.push({
|
|
940
|
+
type: "function_call_output",
|
|
941
|
+
call_id: callId,
|
|
942
|
+
output
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
if (!isLeadingSystemMessage)
|
|
947
|
+
msgIndex++;
|
|
948
|
+
}
|
|
949
|
+
return messages;
|
|
950
|
+
}
|
|
951
|
+
function convertResponsesTools(tools, options) {
|
|
952
|
+
const defaultStrict = options?.strict === undefined ? false : options.strict;
|
|
953
|
+
const supportsStrictMode = options?.supportsStrictMode ?? true;
|
|
954
|
+
const supportsOpenAIGrammarTools = options?.supportsOpenAIGrammarTools ?? false;
|
|
955
|
+
return tools.map((tool) => {
|
|
956
|
+
const grammar = resolveGrammarConstrainedSampling(tool, supportsOpenAIGrammarTools);
|
|
957
|
+
if (grammar) {
|
|
958
|
+
return {
|
|
959
|
+
type: "custom",
|
|
960
|
+
name: tool.name,
|
|
961
|
+
description: tool.description,
|
|
962
|
+
format: {
|
|
963
|
+
type: "grammar",
|
|
964
|
+
syntax: grammar.format,
|
|
965
|
+
definition: grammar.definition
|
|
966
|
+
},
|
|
967
|
+
...options?.toolSearchResult ? { defer_loading: true } : {}
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
const constrainedStrict = resolveJsonSchemaStrictSampling(tool, supportsStrictMode);
|
|
971
|
+
const strict = constrainedStrict ?? defaultStrict;
|
|
972
|
+
const functionTool = {
|
|
973
|
+
type: "function",
|
|
974
|
+
name: tool.name,
|
|
975
|
+
description: tool.description,
|
|
976
|
+
parameters: getJsonSchemaToolParameters(tool, strict === true),
|
|
977
|
+
...options?.toolSearchResult ? { defer_loading: true } : {}
|
|
978
|
+
};
|
|
979
|
+
if (supportsStrictMode) {
|
|
980
|
+
functionTool.strict = strict;
|
|
981
|
+
}
|
|
982
|
+
return functionTool;
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// protocol.ts
|
|
987
|
+
var RESPONSES_APIS = new Set([
|
|
988
|
+
"openai-responses",
|
|
989
|
+
"openai-codex-responses",
|
|
990
|
+
"azure-openai-responses"
|
|
991
|
+
]);
|
|
992
|
+
var MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
|
|
993
|
+
var MAX_ITEM_BYTES = 2 * 1024 * 1024;
|
|
994
|
+
var REMOTE_RETAINED_BYTES = 8 * 1024 * 1024;
|
|
995
|
+
var REMOTE_RETAINED_CHARS = 64000 * 4;
|
|
996
|
+
function isObject(value) {
|
|
997
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
998
|
+
}
|
|
999
|
+
function supportsResponsesApi(model) {
|
|
1000
|
+
return model !== undefined && RESPONSES_APIS.has(String(model.api));
|
|
1001
|
+
}
|
|
1002
|
+
function resolveNativeProtocol(model) {
|
|
1003
|
+
if (!supportsResponsesApi(model))
|
|
1004
|
+
return;
|
|
1005
|
+
return model.api === "openai-codex-responses" ? "remote-v2" : "responses-compact";
|
|
1006
|
+
}
|
|
1007
|
+
function isCompactionItem(value) {
|
|
1008
|
+
return isObject(value) && value.type === "compaction" && typeof value.encrypted_content === "string" && value.encrypted_content.length > 0;
|
|
1009
|
+
}
|
|
1010
|
+
function byteLength(value) {
|
|
1011
|
+
return Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
1012
|
+
}
|
|
1013
|
+
function validateItemSize(item) {
|
|
1014
|
+
if (byteLength(item) > MAX_ITEM_BYTES)
|
|
1015
|
+
throw new Error("Native compaction output item exceeded 2 MiB");
|
|
1016
|
+
}
|
|
1017
|
+
function validContent(value) {
|
|
1018
|
+
return typeof value === "string" || Array.isArray(value) && value.every((part) => {
|
|
1019
|
+
if (!isObject(part))
|
|
1020
|
+
return false;
|
|
1021
|
+
if (part.type === "input_text" || part.type === "output_text" || part.type === "summary_text")
|
|
1022
|
+
return typeof part.text === "string";
|
|
1023
|
+
if (part.type === "input_image")
|
|
1024
|
+
return typeof part.image_url === "string" || typeof part.file_id === "string";
|
|
1025
|
+
if (part.type === "refusal")
|
|
1026
|
+
return typeof part.refusal === "string";
|
|
1027
|
+
return false;
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
function validateCompactedOutput(value) {
|
|
1031
|
+
if (!isObject(value) || !Array.isArray(value.output) || value.output.length === 0) {
|
|
1032
|
+
throw new Error("Responses Compact returned an invalid output window");
|
|
1033
|
+
}
|
|
1034
|
+
if (byteLength(value.output) > MAX_RESPONSE_BYTES)
|
|
1035
|
+
throw new Error("Native compaction output exceeded 8 MiB");
|
|
1036
|
+
const output = value.output.map((item) => {
|
|
1037
|
+
if (!isObject(item))
|
|
1038
|
+
throw new Error("Responses Compact returned a non-object output item");
|
|
1039
|
+
validateItemSize(item);
|
|
1040
|
+
const type = item.type ?? (typeof item.role === "string" ? "message" : undefined);
|
|
1041
|
+
if (!["compaction", "message", "reasoning", "function_call", "function_call_output", "custom_tool_call", "custom_tool_call_output", "tool_search_call", "tool_search_output", "additional_tools"].includes(String(type)))
|
|
1042
|
+
throw new Error("Responses Compact returned an unsupported output item");
|
|
1043
|
+
if (type === "message" && (!["user", "assistant", "system", "developer"].includes(String(item.role)) || !validContent(item.content)))
|
|
1044
|
+
throw new Error("Responses Compact returned an invalid message item");
|
|
1045
|
+
if (type === "compaction" && !isCompactionItem(item))
|
|
1046
|
+
throw new Error("Responses Compact returned an invalid checkpoint");
|
|
1047
|
+
if (type === "reasoning" && !(typeof item.encrypted_content === "string" || Array.isArray(item.summary) && validContent(item.summary)))
|
|
1048
|
+
throw new Error("Responses Compact returned invalid reasoning");
|
|
1049
|
+
if (String(type).includes("call") || type === "tool_search_output") {
|
|
1050
|
+
if (typeof item.call_id !== "string" || !item.call_id)
|
|
1051
|
+
throw new Error("Responses Compact returned an invalid tool id");
|
|
1052
|
+
if (type === "function_call" && (typeof item.name !== "string" || typeof item.arguments !== "string"))
|
|
1053
|
+
throw new Error("Responses Compact returned an invalid function call");
|
|
1054
|
+
if (type === "custom_tool_call" && (typeof item.name !== "string" || typeof item.input !== "string"))
|
|
1055
|
+
throw new Error("Responses Compact returned an invalid custom call");
|
|
1056
|
+
if ((type === "function_call_output" || type === "custom_tool_call_output") && !validContent(item.output))
|
|
1057
|
+
throw new Error("Responses Compact returned an invalid tool output");
|
|
1058
|
+
if (type === "tool_search_call" && !isObject(item.arguments))
|
|
1059
|
+
throw new Error("Responses Compact returned an invalid tool search");
|
|
1060
|
+
}
|
|
1061
|
+
if ((type === "additional_tools" || type === "tool_search_output") && (!Array.isArray(item.tools) || !item.tools.every(isObject)))
|
|
1062
|
+
throw new Error("Responses Compact returned invalid tools");
|
|
1063
|
+
return structuredClone(item);
|
|
1064
|
+
});
|
|
1065
|
+
const checkpoints = output.filter(isCompactionItem);
|
|
1066
|
+
if (checkpoints.length !== 1) {
|
|
1067
|
+
throw new Error(`Responses Compact returned ${checkpoints.length} compaction items; expected exactly one`);
|
|
1068
|
+
}
|
|
1069
|
+
return output;
|
|
1070
|
+
}
|
|
1071
|
+
async function readJsonBounded(response, signal) {
|
|
1072
|
+
const declared = Number(response.headers.get("content-length"));
|
|
1073
|
+
if (Number.isFinite(declared) && declared > MAX_RESPONSE_BYTES) {
|
|
1074
|
+
await response.body?.cancel().catch(() => {
|
|
1075
|
+
return;
|
|
1076
|
+
});
|
|
1077
|
+
throw new Error("Native compaction response exceeded 8 MiB");
|
|
1078
|
+
}
|
|
1079
|
+
if (!response.body)
|
|
1080
|
+
throw new Error("Native compaction response did not contain a body");
|
|
1081
|
+
const reader = response.body.getReader();
|
|
1082
|
+
const chunks = [];
|
|
1083
|
+
let total = 0;
|
|
1084
|
+
const onAbort = () => void reader.cancel(new DOMException("Compaction aborted", "AbortError")).catch(() => {
|
|
1085
|
+
return;
|
|
1086
|
+
});
|
|
1087
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1088
|
+
try {
|
|
1089
|
+
while (true) {
|
|
1090
|
+
if (signal.aborted)
|
|
1091
|
+
throw new DOMException("Compaction aborted", "AbortError");
|
|
1092
|
+
const { done, value } = await reader.read();
|
|
1093
|
+
if (done)
|
|
1094
|
+
break;
|
|
1095
|
+
total += value.byteLength;
|
|
1096
|
+
if (total > MAX_RESPONSE_BYTES)
|
|
1097
|
+
throw new Error("Native compaction response exceeded 8 MiB");
|
|
1098
|
+
chunks.push(value);
|
|
1099
|
+
}
|
|
1100
|
+
} finally {
|
|
1101
|
+
signal.removeEventListener("abort", onAbort);
|
|
1102
|
+
reader.cancel().catch(() => {
|
|
1103
|
+
return;
|
|
1104
|
+
});
|
|
1105
|
+
reader.releaseLock();
|
|
1106
|
+
}
|
|
1107
|
+
const bytes = new Uint8Array(total);
|
|
1108
|
+
let offset = 0;
|
|
1109
|
+
for (const chunk of chunks) {
|
|
1110
|
+
bytes.set(chunk, offset);
|
|
1111
|
+
offset += chunk.byteLength;
|
|
1112
|
+
}
|
|
1113
|
+
try {
|
|
1114
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
1115
|
+
} catch {
|
|
1116
|
+
throw new Error("Native compaction response returned malformed JSON");
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
function requestUrl(input) {
|
|
1120
|
+
return new URL(input instanceof Request ? input.url : String(input));
|
|
1121
|
+
}
|
|
1122
|
+
function responsesCompactUrl(input) {
|
|
1123
|
+
const original = requestUrl(input);
|
|
1124
|
+
if (!original.pathname.endsWith("/responses")) {
|
|
1125
|
+
throw new Error("Provider request URL does not end with /responses");
|
|
1126
|
+
}
|
|
1127
|
+
const compact = new URL(original);
|
|
1128
|
+
compact.pathname = `${compact.pathname}/compact`;
|
|
1129
|
+
if (compact.origin !== original.origin)
|
|
1130
|
+
throw new Error("Responses Compact URL changed origin");
|
|
1131
|
+
return compact;
|
|
1132
|
+
}
|
|
1133
|
+
function mergedHeaders(input, init) {
|
|
1134
|
+
const headers = new Headers(input instanceof Request ? input.headers : undefined);
|
|
1135
|
+
new Headers(init?.headers).forEach((value, name) => headers.set(name, value));
|
|
1136
|
+
headers.delete("content-encoding");
|
|
1137
|
+
headers.delete("content-length");
|
|
1138
|
+
headers.set("accept", "application/json");
|
|
1139
|
+
headers.set("content-type", "application/json");
|
|
1140
|
+
return headers;
|
|
1141
|
+
}
|
|
1142
|
+
function compactPayload(payload) {
|
|
1143
|
+
const allowed = [
|
|
1144
|
+
"model",
|
|
1145
|
+
"input",
|
|
1146
|
+
"instructions",
|
|
1147
|
+
"previous_response_id",
|
|
1148
|
+
"prompt_cache_key",
|
|
1149
|
+
"prompt_cache_retention",
|
|
1150
|
+
"service_tier"
|
|
1151
|
+
];
|
|
1152
|
+
const result = {};
|
|
1153
|
+
for (const key of allowed) {
|
|
1154
|
+
if (Object.hasOwn(payload, key) && payload[key] !== undefined)
|
|
1155
|
+
result[key] = structuredClone(payload[key]);
|
|
1156
|
+
}
|
|
1157
|
+
if (typeof result.model !== "string" || !Array.isArray(result.input)) {
|
|
1158
|
+
throw new Error("Provider did not expose a valid Responses payload for compaction");
|
|
1159
|
+
}
|
|
1160
|
+
return result;
|
|
1161
|
+
}
|
|
1162
|
+
function errorResponse(error) {
|
|
1163
|
+
return Response.json({ error: { message: error instanceof Error ? error.message : String(error), type: "invalid_request_error" } }, { status: 400 });
|
|
1164
|
+
}
|
|
1165
|
+
function rawUsage(response) {
|
|
1166
|
+
if (!isObject(response.usage))
|
|
1167
|
+
throw new Error("Native compaction response is missing usage");
|
|
1168
|
+
return structuredClone(response.usage);
|
|
1169
|
+
}
|
|
1170
|
+
function syntheticCompletion(response, payload) {
|
|
1171
|
+
const completed = {
|
|
1172
|
+
id: typeof response.id === "string" ? response.id : "resp_pi_provider_compaction",
|
|
1173
|
+
object: "response",
|
|
1174
|
+
created_at: typeof response.created_at === "number" ? response.created_at : Math.floor(Date.now() / 1000),
|
|
1175
|
+
status: "completed",
|
|
1176
|
+
model: payload.model,
|
|
1177
|
+
output: [],
|
|
1178
|
+
parallel_tool_calls: false,
|
|
1179
|
+
tool_choice: "auto",
|
|
1180
|
+
tools: [],
|
|
1181
|
+
usage: rawUsage(response)
|
|
1182
|
+
};
|
|
1183
|
+
const body = [
|
|
1184
|
+
{ type: "response.created", response: { ...completed, status: "in_progress" } },
|
|
1185
|
+
{ type: "response.completed", response: completed }
|
|
1186
|
+
].map((event) => `data: ${JSON.stringify(event)}
|
|
1187
|
+
|
|
1188
|
+
`).join("");
|
|
1189
|
+
return new Response(body, { status: 200, headers: { "content-type": "text/event-stream" } });
|
|
1190
|
+
}
|
|
1191
|
+
async function collectProviderUsage(stream, signal) {
|
|
1192
|
+
let usage;
|
|
1193
|
+
for await (const event of stream) {
|
|
1194
|
+
if (signal.aborted)
|
|
1195
|
+
throw new DOMException("Compaction aborted", "AbortError");
|
|
1196
|
+
if (event.type === "error")
|
|
1197
|
+
throw new Error(event.error.errorMessage ?? "Native compaction provider request failed");
|
|
1198
|
+
if (event.type === "done")
|
|
1199
|
+
usage = event.message.usage;
|
|
1200
|
+
}
|
|
1201
|
+
if (!usage)
|
|
1202
|
+
throw new Error("Native compaction provider stream ended without usage");
|
|
1203
|
+
return usage;
|
|
1204
|
+
}
|
|
1205
|
+
async function requestResponsesCompact(request) {
|
|
1206
|
+
let preparedPayload;
|
|
1207
|
+
let compacted;
|
|
1208
|
+
let bridgeError;
|
|
1209
|
+
let dispatches = 0;
|
|
1210
|
+
const baseFetch = request.fetch ?? globalThis.fetch;
|
|
1211
|
+
const bridgeFetch = async (input, init) => {
|
|
1212
|
+
if (request.signal.aborted)
|
|
1213
|
+
throw new DOMException("Compaction aborted", "AbortError");
|
|
1214
|
+
dispatches += 1;
|
|
1215
|
+
if (dispatches !== 1 || !preparedPayload) {
|
|
1216
|
+
bridgeError = new Error("Responses Compact provider dispatched an unexpected additional request");
|
|
1217
|
+
return errorResponse(bridgeError);
|
|
1218
|
+
}
|
|
1219
|
+
try {
|
|
1220
|
+
const compactUrl = responsesCompactUrl(input);
|
|
1221
|
+
const response = await baseFetch(compactUrl, {
|
|
1222
|
+
...init,
|
|
1223
|
+
method: "POST",
|
|
1224
|
+
headers: mergedHeaders(input, init),
|
|
1225
|
+
body: JSON.stringify(compactPayload(preparedPayload)),
|
|
1226
|
+
signal: request.signal
|
|
1227
|
+
});
|
|
1228
|
+
if (!response.ok)
|
|
1229
|
+
return response;
|
|
1230
|
+
const parsed = await readJsonBounded(response, request.signal);
|
|
1231
|
+
if (!isObject(parsed))
|
|
1232
|
+
throw new Error("Responses Compact returned a non-object response");
|
|
1233
|
+
compacted = { response: parsed, output: validateCompactedOutput(parsed) };
|
|
1234
|
+
return syntheticCompletion(parsed, preparedPayload);
|
|
1235
|
+
} catch (error) {
|
|
1236
|
+
bridgeError = error;
|
|
1237
|
+
return errorResponse(error);
|
|
1238
|
+
}
|
|
1239
|
+
};
|
|
1240
|
+
const stream = request.provider.stream(request.model, request.context, {
|
|
1241
|
+
apiKey: request.apiKey,
|
|
1242
|
+
headers: request.headers,
|
|
1243
|
+
env: request.env,
|
|
1244
|
+
signal: request.signal,
|
|
1245
|
+
transport: "sse",
|
|
1246
|
+
cacheRetention: "none",
|
|
1247
|
+
maxRetries: 0,
|
|
1248
|
+
fetch: bridgeFetch,
|
|
1249
|
+
onPayload: (payload) => {
|
|
1250
|
+
if (!isObject(payload))
|
|
1251
|
+
throw new Error("Responses provider exposed a non-object payload");
|
|
1252
|
+
if (request.preparedPayload && request.preparedPayload.model !== payload.model)
|
|
1253
|
+
throw new Error("Native route model changed since capture");
|
|
1254
|
+
preparedPayload = { ...structuredClone(request.preparedPayload ?? payload), input: structuredClone(request.input) };
|
|
1255
|
+
return preparedPayload;
|
|
1256
|
+
}
|
|
1257
|
+
});
|
|
1258
|
+
const usage = await collectProviderUsage(stream, request.signal).catch((error) => {
|
|
1259
|
+
if (bridgeError)
|
|
1260
|
+
throw bridgeError;
|
|
1261
|
+
throw error;
|
|
1262
|
+
});
|
|
1263
|
+
if (bridgeError)
|
|
1264
|
+
throw bridgeError;
|
|
1265
|
+
if (!compacted || dispatches !== 1)
|
|
1266
|
+
throw new Error("Responses Compact did not complete exactly one request");
|
|
1267
|
+
return { output: compacted.output, usage };
|
|
1268
|
+
}
|
|
1269
|
+
async function collectCompactionSse(stream, signal) {
|
|
1270
|
+
const reader = stream.getReader();
|
|
1271
|
+
const decoder = new TextDecoder;
|
|
1272
|
+
let bytes = 0;
|
|
1273
|
+
let pending = "";
|
|
1274
|
+
let dataLines = [];
|
|
1275
|
+
let completed = false;
|
|
1276
|
+
const items = new Map;
|
|
1277
|
+
const onAbort = () => void reader.cancel(new DOMException("Compaction aborted", "AbortError")).catch(() => {
|
|
1278
|
+
return;
|
|
1279
|
+
});
|
|
1280
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1281
|
+
const dispatch = () => {
|
|
1282
|
+
if (dataLines.length === 0)
|
|
1283
|
+
return;
|
|
1284
|
+
const data = dataLines.join(`
|
|
1285
|
+
`);
|
|
1286
|
+
dataLines = [];
|
|
1287
|
+
if (data === "[DONE]")
|
|
1288
|
+
return;
|
|
1289
|
+
let event;
|
|
1290
|
+
try {
|
|
1291
|
+
event = JSON.parse(data);
|
|
1292
|
+
} catch {
|
|
1293
|
+
throw new Error("Remote V2 returned malformed SSE JSON");
|
|
1294
|
+
}
|
|
1295
|
+
if (!isObject(event))
|
|
1296
|
+
return;
|
|
1297
|
+
if (event.type === "response.completed")
|
|
1298
|
+
completed = true;
|
|
1299
|
+
const candidates = [];
|
|
1300
|
+
if (event.type === "response.output_item.done")
|
|
1301
|
+
candidates.push(event.item);
|
|
1302
|
+
if (event.type === "response.completed" && isObject(event.response) && Array.isArray(event.response.output)) {
|
|
1303
|
+
candidates.push(...event.response.output);
|
|
1304
|
+
}
|
|
1305
|
+
for (const candidate of candidates) {
|
|
1306
|
+
if (!isCompactionItem(candidate))
|
|
1307
|
+
continue;
|
|
1308
|
+
validateItemSize(candidate);
|
|
1309
|
+
items.set(JSON.stringify(candidate), structuredClone(candidate));
|
|
1310
|
+
}
|
|
1311
|
+
};
|
|
1312
|
+
const processLine = (line) => {
|
|
1313
|
+
if (line === "")
|
|
1314
|
+
return dispatch();
|
|
1315
|
+
if (line.startsWith(":"))
|
|
1316
|
+
return;
|
|
1317
|
+
if (line.startsWith("data:"))
|
|
1318
|
+
dataLines.push(line.slice(5).replace(/^ /, ""));
|
|
1319
|
+
};
|
|
1320
|
+
try {
|
|
1321
|
+
while (true) {
|
|
1322
|
+
if (signal.aborted)
|
|
1323
|
+
throw new DOMException("Compaction aborted", "AbortError");
|
|
1324
|
+
const { done, value } = await reader.read();
|
|
1325
|
+
if (done)
|
|
1326
|
+
break;
|
|
1327
|
+
bytes += value.byteLength;
|
|
1328
|
+
if (bytes > MAX_RESPONSE_BYTES)
|
|
1329
|
+
throw new Error("Remote V2 stream exceeded 8 MiB");
|
|
1330
|
+
pending += decoder.decode(value, { stream: true });
|
|
1331
|
+
let newline = pending.indexOf(`
|
|
1332
|
+
`);
|
|
1333
|
+
while (newline >= 0) {
|
|
1334
|
+
const raw = pending.slice(0, newline);
|
|
1335
|
+
pending = pending.slice(newline + 1);
|
|
1336
|
+
processLine(raw.endsWith("\r") ? raw.slice(0, -1) : raw);
|
|
1337
|
+
newline = pending.indexOf(`
|
|
1338
|
+
`);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
pending += decoder.decode();
|
|
1342
|
+
if (pending)
|
|
1343
|
+
processLine(pending.endsWith("\r") ? pending.slice(0, -1) : pending);
|
|
1344
|
+
dispatch();
|
|
1345
|
+
} finally {
|
|
1346
|
+
signal.removeEventListener("abort", onAbort);
|
|
1347
|
+
reader.cancel().catch(() => {
|
|
1348
|
+
return;
|
|
1349
|
+
});
|
|
1350
|
+
reader.releaseLock();
|
|
1351
|
+
}
|
|
1352
|
+
if (!completed)
|
|
1353
|
+
throw new Error("Remote V2 stream ended without response.completed");
|
|
1354
|
+
if (items.size !== 1)
|
|
1355
|
+
throw new Error(`Remote V2 returned ${items.size} distinct compaction items; expected exactly one`);
|
|
1356
|
+
return { item: [...items.values()][0] };
|
|
1357
|
+
}
|
|
1358
|
+
function retainedUserMessage(item) {
|
|
1359
|
+
return (item.type === undefined || item.type === "message") && item.role === "user" && Array.isArray(item.content);
|
|
1360
|
+
}
|
|
1361
|
+
function buildRemoteV2History(input, item) {
|
|
1362
|
+
validateItemSize(item);
|
|
1363
|
+
let remainingBytes = REMOTE_RETAINED_BYTES - byteLength(item);
|
|
1364
|
+
let remainingChars = REMOTE_RETAINED_CHARS;
|
|
1365
|
+
const kept = [];
|
|
1366
|
+
for (let index = input.length - 1;index >= 0; index--) {
|
|
1367
|
+
const candidate = input[index];
|
|
1368
|
+
if (!retainedUserMessage(candidate))
|
|
1369
|
+
continue;
|
|
1370
|
+
const bytes = byteLength(candidate);
|
|
1371
|
+
if (bytes > remainingBytes)
|
|
1372
|
+
continue;
|
|
1373
|
+
const chars = JSON.stringify(candidate).length;
|
|
1374
|
+
if (chars > remainingChars)
|
|
1375
|
+
continue;
|
|
1376
|
+
kept.push(structuredClone(candidate));
|
|
1377
|
+
remainingBytes -= bytes;
|
|
1378
|
+
remainingChars -= chars;
|
|
1379
|
+
if (remainingBytes < 256 || remainingChars < 64)
|
|
1380
|
+
break;
|
|
1381
|
+
}
|
|
1382
|
+
return [...kept.reverse(), structuredClone(item)];
|
|
1383
|
+
}
|
|
1384
|
+
async function requestRemoteV2(request) {
|
|
1385
|
+
const baseFetch = request.fetch ?? globalThis.fetch;
|
|
1386
|
+
let sentInput;
|
|
1387
|
+
const controller = new AbortController;
|
|
1388
|
+
const signal = AbortSignal.any([request.signal, controller.signal]);
|
|
1389
|
+
const inspections = [];
|
|
1390
|
+
const inspectedFetch = async (input, init) => {
|
|
1391
|
+
const response = await baseFetch(input, init);
|
|
1392
|
+
if (!response.ok || !response.body)
|
|
1393
|
+
return response;
|
|
1394
|
+
const [providerBody, inspectionBody] = response.body.tee();
|
|
1395
|
+
inspections.push(collectCompactionSse(inspectionBody, signal).then((result) => ({ result }), (error) => ({ error })));
|
|
1396
|
+
return new Response(providerBody, {
|
|
1397
|
+
status: response.status,
|
|
1398
|
+
statusText: response.statusText,
|
|
1399
|
+
headers: response.headers
|
|
1400
|
+
});
|
|
1401
|
+
};
|
|
1402
|
+
const stream = request.provider.stream(request.model, request.context, {
|
|
1403
|
+
apiKey: request.apiKey,
|
|
1404
|
+
headers: request.headers,
|
|
1405
|
+
env: request.env,
|
|
1406
|
+
signal: request.signal,
|
|
1407
|
+
transport: "sse",
|
|
1408
|
+
cacheRetention: "none",
|
|
1409
|
+
maxRetries: 0,
|
|
1410
|
+
fetch: inspectedFetch,
|
|
1411
|
+
onPayload: (payload) => {
|
|
1412
|
+
if (!isObject(payload))
|
|
1413
|
+
throw new Error("Codex provider exposed a non-object payload");
|
|
1414
|
+
sentInput = structuredClone(request.input);
|
|
1415
|
+
if (request.preparedPayload && request.preparedPayload.model !== payload.model)
|
|
1416
|
+
throw new Error("Native route model changed since capture");
|
|
1417
|
+
return { ...structuredClone(request.preparedPayload ?? payload), stream: true, input: [...sentInput, { type: "compaction_trigger" }] };
|
|
1418
|
+
}
|
|
1419
|
+
});
|
|
1420
|
+
try {
|
|
1421
|
+
const usage = await collectProviderUsage(stream, request.signal);
|
|
1422
|
+
if (!sentInput || inspections.length !== 1)
|
|
1423
|
+
throw new Error(`Remote V2 observed ${inspections.length} successful provider responses; expected exactly one`);
|
|
1424
|
+
const inspection = await inspections[0];
|
|
1425
|
+
if ("error" in inspection)
|
|
1426
|
+
throw inspection.error;
|
|
1427
|
+
return { output: buildRemoteV2History(sentInput, inspection.result.item), usage };
|
|
1428
|
+
} finally {
|
|
1429
|
+
controller.abort();
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
async function requestProviderCompaction(request) {
|
|
1433
|
+
const result = await (request.protocol === "remote-v2" ? requestRemoteV2(request) : requestResponsesCompact(request));
|
|
1434
|
+
return { ...result, output: validateCompactedOutput(result) };
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// replay.ts
|
|
1438
|
+
var TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]);
|
|
1439
|
+
var MAX_SNAPSHOT_BYTES = 8 * 1024 * 1024;
|
|
1440
|
+
var SNAPSHOT_TTL_MS = 5 * 60000;
|
|
1441
|
+
function equal(left, right) {
|
|
1442
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
1443
|
+
}
|
|
1444
|
+
function serializeInput(model, context, declarations = context) {
|
|
1445
|
+
const compat = model.compat;
|
|
1446
|
+
const flag = (name) => compat?.[name] === true;
|
|
1447
|
+
const codex = model.api === "openai-codex-responses";
|
|
1448
|
+
const normalized = resolveTranscript2(context, flag("supportsMidConvoSystemMessages"));
|
|
1449
|
+
const tools = resolveTranscript2(declarations, flag("supportsMidConvoSystemMessages"));
|
|
1450
|
+
return convertResponsesMessages(model, normalized, model.api === "azure-openai-responses" ? new Set([...TOOL_CALL_PROVIDERS, "azure-openai-responses"]) : TOOL_CALL_PROVIDERS, {
|
|
1451
|
+
includeSystemPrompt: !codex,
|
|
1452
|
+
grammarToolInputProperties: createGrammarToolInputProperties(getDeclaredTools(tools.messages), flag("supportsOpenAIGrammarTools")),
|
|
1453
|
+
supportsMidConvoSystemMessages: flag("supportsMidConvoSystemMessages"),
|
|
1454
|
+
supportsAdditionalTools: flag("supportsAdditionalTools"),
|
|
1455
|
+
supportsToolSearch: flag("supportsToolSearch"),
|
|
1456
|
+
toolOptions: {
|
|
1457
|
+
...codex ? { strict: null } : {},
|
|
1458
|
+
supportsStrictMode: compat?.supportsStrictMode ?? model.api !== "openai-responses",
|
|
1459
|
+
supportsOpenAIGrammarTools: flag("supportsOpenAIGrammarTools")
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
function capturePreparedRequest(model, context, canonical, payload, reject = () => {}) {
|
|
1464
|
+
if (!isObject(payload) || !Array.isArray(payload.input) || !payload.input.every(isObject)) {
|
|
1465
|
+
reject("unsupported payload shape");
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
if (payload.previous_response_id || payload.conversation) {
|
|
1469
|
+
reject("stateful request");
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
if (Buffer.byteLength(JSON.stringify({ context, payload })) > MAX_SNAPSHOT_BYTES) {
|
|
1473
|
+
reject("snapshot exceeds size limit");
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
const expected = serializeInput(model, context);
|
|
1477
|
+
if (!equal(expected, payload.input)) {
|
|
1478
|
+
reject("prepared input differs from serializer");
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
return { context: structuredClone(context), canonical: structuredClone(canonical), payload: structuredClone(payload), portableInput: expected, createdAt: Date.now() };
|
|
1482
|
+
}
|
|
1483
|
+
function promptCount(input) {
|
|
1484
|
+
let count = 0;
|
|
1485
|
+
while (input[count]?.role === "system" || input[count]?.role === "developer")
|
|
1486
|
+
count++;
|
|
1487
|
+
return count;
|
|
1488
|
+
}
|
|
1489
|
+
function closedTools(context) {
|
|
1490
|
+
const pending = new Set;
|
|
1491
|
+
for (const message of context.messages) {
|
|
1492
|
+
if (message.role === "assistant") {
|
|
1493
|
+
if (message.stopReason === "error" || message.stopReason === "aborted")
|
|
1494
|
+
return false;
|
|
1495
|
+
for (const block of message.content)
|
|
1496
|
+
if (block.type === "toolCall") {
|
|
1497
|
+
if (pending.has(block.id))
|
|
1498
|
+
return false;
|
|
1499
|
+
pending.add(block.id);
|
|
1500
|
+
}
|
|
1501
|
+
} else if (message.role === "toolResult") {
|
|
1502
|
+
if (!pending.delete(message.toolCallId))
|
|
1503
|
+
return false;
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
return pending.size === 0;
|
|
1507
|
+
}
|
|
1508
|
+
function mappedPrefix(snapshot, prefix) {
|
|
1509
|
+
if (!equal(snapshot.canonical.messages.slice(0, prefix.messages.length), prefix.messages))
|
|
1510
|
+
return;
|
|
1511
|
+
const head = prefix.messages[0]?.role === "system" ? 1 : 0;
|
|
1512
|
+
if (head && snapshot.context.messages[0]?.role !== "system")
|
|
1513
|
+
return;
|
|
1514
|
+
if (!equal(snapshot.context.messages.slice(head, prefix.messages.length), prefix.messages.slice(head)))
|
|
1515
|
+
return;
|
|
1516
|
+
return normalizeContext({ messages: snapshot.context.messages.slice(0, prefix.messages.length) });
|
|
1517
|
+
}
|
|
1518
|
+
function selectCoveredPrefix(model, snapshot, prefix, reject = () => {}) {
|
|
1519
|
+
if (Date.now() - snapshot.createdAt > SNAPSHOT_TTL_MS) {
|
|
1520
|
+
reject("snapshot expired");
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
if (!closedTools(prefix)) {
|
|
1524
|
+
reject("prefix has an incomplete tool exchange or failed assistant");
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
const mapped = mappedPrefix(snapshot, prefix);
|
|
1528
|
+
if (!mapped) {
|
|
1529
|
+
reject("projected prefix differs from request context");
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
const expected = serializeInput(model, mapped, snapshot.context);
|
|
1533
|
+
if (!equal(snapshot.portableInput.slice(0, expected.length), expected)) {
|
|
1534
|
+
reject("serialized prefix does not match prepared input");
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
const start = promptCount(expected);
|
|
1538
|
+
let end = expected.length;
|
|
1539
|
+
if (snapshot.replacement) {
|
|
1540
|
+
if (snapshot.replacement.index < start || snapshot.replacement.index >= end) {
|
|
1541
|
+
reject("previous native slot is outside covered prefix");
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
end += snapshot.replacement.count - 1;
|
|
1545
|
+
}
|
|
1546
|
+
if (end <= start) {
|
|
1547
|
+
reject("empty covered prefix");
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
return structuredClone(snapshot.payload.input.slice(start, end));
|
|
1551
|
+
}
|
|
1552
|
+
function substituteSummary(payload, index, summary, output) {
|
|
1553
|
+
if (!isObject(payload) || !Array.isArray(payload.input) || !payload.input.every(isObject))
|
|
1554
|
+
return;
|
|
1555
|
+
if (!equal(payload.input[index], summary))
|
|
1556
|
+
return;
|
|
1557
|
+
if (payload.input.filter((item) => equal(item, summary)).length !== 1)
|
|
1558
|
+
return;
|
|
1559
|
+
return { ...payload, input: [...payload.input.slice(0, index), ...structuredClone(output), ...payload.input.slice(index + 1)] };
|
|
1560
|
+
}
|
|
1561
|
+
function summarySlot(model, snapshot, summaryPrefix) {
|
|
1562
|
+
const mapped = mappedPrefix(snapshot, summaryPrefix);
|
|
1563
|
+
if (!mapped)
|
|
1564
|
+
return;
|
|
1565
|
+
const prefix = serializeInput(model, mapped, snapshot.context);
|
|
1566
|
+
const index = promptCount(prefix);
|
|
1567
|
+
if (prefix.length !== index + 1 || !equal(snapshot.portableInput.slice(0, prefix.length), prefix))
|
|
1568
|
+
return;
|
|
1569
|
+
return index;
|
|
1570
|
+
}
|
|
1571
|
+
function requestPolicy(model, snapshot) {
|
|
1572
|
+
return {
|
|
1573
|
+
model,
|
|
1574
|
+
system: getCurrentSystemPrompt(snapshot.context.messages),
|
|
1575
|
+
currentTools: getCurrentTools2(snapshot.context.messages),
|
|
1576
|
+
declarations: getDeclaredTools(resolveTranscript2(snapshot.context, model.compat?.supportsMidConvoSystemMessages).messages),
|
|
1577
|
+
instructions: snapshot.payload.instructions,
|
|
1578
|
+
tools: snapshot.payload.tools,
|
|
1579
|
+
prompt: snapshot.portableInput.slice(0, promptCount(snapshot.portableInput))
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
// index.ts
|
|
1584
|
+
var NATIVE_COMPACTION_TYPE = "pi-provider-compaction/openai-responses";
|
|
1585
|
+
var NATIVE_COMPACTION_VERSION = 3;
|
|
1586
|
+
var NATIVE_COMPACTION_SUMMARY = "This context was compacted using the provider's native OpenAI Responses state.";
|
|
1587
|
+
var supportsNativeCompaction = supportsResponsesApi;
|
|
1588
|
+
var digest = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
1589
|
+
var transcript = (messages) => normalizeContext2({ messages: convertToLlm(messages) });
|
|
1590
|
+
function readNativeCompactionDetails(value) {
|
|
1591
|
+
if (!isObject(value) || value.type !== NATIVE_COMPACTION_TYPE || value.version !== 1 && value.version !== 2 && value.version !== 3)
|
|
1592
|
+
return;
|
|
1593
|
+
if (typeof value.provider !== "string" || typeof value.api !== "string" || typeof value.model !== "string")
|
|
1594
|
+
return;
|
|
1595
|
+
const protocol = value.version === 1 ? "responses-compact" : value.protocol;
|
|
1596
|
+
if (protocol !== "responses-compact" && protocol !== "remote-v2")
|
|
1597
|
+
return;
|
|
1598
|
+
try {
|
|
1599
|
+
const output = validateCompactedOutput(value);
|
|
1600
|
+
if (value.version === 3) {
|
|
1601
|
+
const coverage = value.coverage;
|
|
1602
|
+
if (!isObject(coverage) || coverage.kind !== "summarized-prefix")
|
|
1603
|
+
return;
|
|
1604
|
+
for (const field of ["firstKeptEntryId", "sourceLeafId", "sourceHash", "policyHash", "inputHash", "runtimeId"]) {
|
|
1605
|
+
if (typeof coverage[field] !== "string" || !coverage[field])
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
if (!isObject(value.identity) || typeof value.identity.endpoint !== "string")
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
return { ...structuredClone(value), protocol, output };
|
|
1612
|
+
} catch {
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
function latestCompaction(branch) {
|
|
1617
|
+
for (let index = branch.length - 1;index >= 0; index--) {
|
|
1618
|
+
const entry = branch[index];
|
|
1619
|
+
if (entry.type === "compaction")
|
|
1620
|
+
return entry;
|
|
1621
|
+
}
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
function header(headers, name) {
|
|
1625
|
+
const value = Object.entries(headers ?? {}).find(([key]) => key.toLowerCase() === name)?.[1];
|
|
1626
|
+
return typeof value === "string" && value ? value : undefined;
|
|
1627
|
+
}
|
|
1628
|
+
function identity(model, options) {
|
|
1629
|
+
const azure = model.api === "azure-openai-responses";
|
|
1630
|
+
const azureBase = options.env?.AZURE_OPENAI_BASE_URL || (options.env?.AZURE_OPENAI_RESOURCE_NAME ? `https://${options.env.AZURE_OPENAI_RESOURCE_NAME}.openai.azure.com/openai/v1` : undefined);
|
|
1631
|
+
const url = new URL(azure ? azureBase ?? model.baseUrl : model.baseUrl);
|
|
1632
|
+
url.username = "";
|
|
1633
|
+
url.password = "";
|
|
1634
|
+
url.search = "";
|
|
1635
|
+
url.hash = "";
|
|
1636
|
+
let accountId = header(options.headers, "chatgpt-account-id");
|
|
1637
|
+
if (!accountId && model.api === "openai-codex-responses" && options.apiKey) {
|
|
1638
|
+
try {
|
|
1639
|
+
const token = JSON.parse(Buffer.from(options.apiKey.split(".")[1], "base64url").toString("utf8"));
|
|
1640
|
+
const id = token["https://api.openai.com/auth"]?.chatgpt_account_id;
|
|
1641
|
+
if (typeof id === "string")
|
|
1642
|
+
accountId = id;
|
|
1643
|
+
} catch {}
|
|
1644
|
+
}
|
|
1645
|
+
const map = options.env?.AZURE_OPENAI_DEPLOYMENT_NAME_MAP?.split(",").map((entry) => entry.split("=").map((part) => part.trim()));
|
|
1646
|
+
return {
|
|
1647
|
+
endpoint: url.toString().replace(/\/+$/, ""),
|
|
1648
|
+
organization: header(options.headers, "openai-organization"),
|
|
1649
|
+
project: header(options.headers, "openai-project"),
|
|
1650
|
+
accountId,
|
|
1651
|
+
azureApiVersion: azure ? options.env?.AZURE_OPENAI_API_VERSION ?? "v1" : undefined,
|
|
1652
|
+
azureDeployment: azure ? map?.find(([id]) => id === model.id)?.[1] ?? model.id : undefined
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
function combineUsage(first, second) {
|
|
1656
|
+
if (!first)
|
|
1657
|
+
return second;
|
|
1658
|
+
if (!second)
|
|
1659
|
+
return first;
|
|
1660
|
+
const result = structuredClone(first);
|
|
1661
|
+
for (const key of ["input", "output", "cacheRead", "cacheWrite", "totalTokens"])
|
|
1662
|
+
result[key] += second[key];
|
|
1663
|
+
for (const key of ["reasoning", "cacheWrite1h"])
|
|
1664
|
+
if (first[key] !== undefined || second[key] !== undefined)
|
|
1665
|
+
result[key] = (first[key] ?? 0) + (second[key] ?? 0);
|
|
1666
|
+
for (const key of ["input", "output", "cacheRead", "cacheWrite", "total"])
|
|
1667
|
+
result.cost[key] += second.cost[key];
|
|
1668
|
+
return result;
|
|
1669
|
+
}
|
|
1670
|
+
function supportedRouteOptions(model, options) {
|
|
1671
|
+
if (model.api !== "azure-openai-responses")
|
|
1672
|
+
return true;
|
|
1673
|
+
const extra = options;
|
|
1674
|
+
if (["azureBaseUrl", "azureResourceName", "azureApiVersion", "azureDeploymentName"].some((key) => extra[key] !== undefined))
|
|
1675
|
+
return false;
|
|
1676
|
+
return !["AZURE_OPENAI_BASE_URL", "AZURE_OPENAI_RESOURCE_NAME", "AZURE_OPENAI_API_VERSION", "AZURE_OPENAI_DEPLOYMENT_NAME_MAP"].some((key) => process.env[key] !== undefined && options.env?.[key] === undefined);
|
|
1677
|
+
}
|
|
1678
|
+
function pi_provider_compaction_default(pi) {
|
|
1679
|
+
const secret = randomBytes(32);
|
|
1680
|
+
const routeDigest = (value) => createHmac("sha256", secret).update(JSON.stringify(value)).digest("hex");
|
|
1681
|
+
let runtimeId = randomUUID();
|
|
1682
|
+
let ctx;
|
|
1683
|
+
let snapshot;
|
|
1684
|
+
let active;
|
|
1685
|
+
let pending;
|
|
1686
|
+
let uninstall;
|
|
1687
|
+
let installedProvider;
|
|
1688
|
+
let generation = 0;
|
|
1689
|
+
let captureStatus = "no request observed";
|
|
1690
|
+
let compactionStatus = "not attempted in this runtime";
|
|
1691
|
+
const captureRejected = (reason) => {
|
|
1692
|
+
captureStatus = reason;
|
|
1693
|
+
};
|
|
1694
|
+
const fallback = (reason) => {
|
|
1695
|
+
compactionStatus = `portable: ${reason}`;
|
|
1696
|
+
};
|
|
1697
|
+
pi.registerCommand("provider-compaction-status", {
|
|
1698
|
+
description: "Show content-free native compaction eligibility and last attempt",
|
|
1699
|
+
handler: async (_args, context) => {
|
|
1700
|
+
context.ui.notify(`Provider compaction
|
|
1701
|
+
Observer: ${installedProvider ? "installed" : "unavailable for current registration"}
|
|
1702
|
+
Capture: ${captureStatus}
|
|
1703
|
+
Last attempt: ${compactionStatus}`, "info");
|
|
1704
|
+
}
|
|
1705
|
+
});
|
|
1706
|
+
const invalidate = () => {
|
|
1707
|
+
generation++;
|
|
1708
|
+
runtimeId = randomUUID();
|
|
1709
|
+
snapshot = undefined;
|
|
1710
|
+
active = undefined;
|
|
1711
|
+
pending = undefined;
|
|
1712
|
+
captureStatus = "invalidated by lifecycle/history/policy change";
|
|
1713
|
+
};
|
|
1714
|
+
const generic = (context) => resolveCompactionModelPolicy(pi, context).hasSelectors;
|
|
1715
|
+
const route = (model, options) => routeDigest({ model, apiKey: options.apiKey, headers: options.headers, env: options.env });
|
|
1716
|
+
const unchanged = (branch, length, hash) => branch.length >= length && digest(branch.slice(0, length)) === hash && !branch.slice(length).some((entry) => entry.type === "context_edit");
|
|
1717
|
+
const observe = (model, context, options) => {
|
|
1718
|
+
const owner = ctx;
|
|
1719
|
+
if (!owner || options.sessionId !== owner.sessionManager.getSessionId())
|
|
1720
|
+
return;
|
|
1721
|
+
if (!supportsNativeCompaction(model)) {
|
|
1722
|
+
captureRejected("unsupported API");
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
if (!options.onPayload) {
|
|
1726
|
+
captureRejected("no payload callback");
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
if (generic(owner)) {
|
|
1730
|
+
captureRejected("generic compaction model takes precedence");
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
if (!supportedRouteOptions(model, options)) {
|
|
1734
|
+
captureRejected("unsupported route overrides");
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
const ticket = generation;
|
|
1738
|
+
const branch = owner.sessionManager.getBranch();
|
|
1739
|
+
const branchHash = digest(branch);
|
|
1740
|
+
const canonical = transcript(owner.sessionManager.buildSessionProjection().messages);
|
|
1741
|
+
const current = () => generation === ticket && ctx === owner && !options.signal?.aborted && supportedRouteOptions(model, options) && digest(owner.sessionManager.getBranch()) === branchHash;
|
|
1742
|
+
return async (payload) => {
|
|
1743
|
+
if (!current())
|
|
1744
|
+
return payload;
|
|
1745
|
+
snapshot = undefined;
|
|
1746
|
+
const prepared = capturePreparedRequest(model, context, canonical, payload, captureRejected);
|
|
1747
|
+
if (!prepared) {
|
|
1748
|
+
active = undefined;
|
|
1749
|
+
return payload;
|
|
1750
|
+
}
|
|
1751
|
+
const auth = await owner.modelRegistry.getApiKeyAndHeaders(model);
|
|
1752
|
+
if (!current())
|
|
1753
|
+
return payload;
|
|
1754
|
+
const normalizedHeaders = (headers) => Object.entries(headers ?? {}).map(([name, value]) => [name.toLowerCase(), value]).sort(([a], [b]) => String(a).localeCompare(String(b)));
|
|
1755
|
+
if (!auth.ok || generic(owner) || auth.apiKey !== options.apiKey || (auth.baseUrl ?? model.baseUrl) !== model.baseUrl || !equal(normalizedHeaders(auth.headers), normalizedHeaders(options.headers)) || !equal(auth.env ?? {}, options.env ?? {})) {
|
|
1756
|
+
active = undefined;
|
|
1757
|
+
captureRejected("resolved auth/route differs from prepared request, or generic policy changed");
|
|
1758
|
+
return payload;
|
|
1759
|
+
}
|
|
1760
|
+
const provider = owner.modelRegistry.getProvider(model.provider);
|
|
1761
|
+
if (!provider) {
|
|
1762
|
+
captureRejected("provider unavailable");
|
|
1763
|
+
return payload;
|
|
1764
|
+
}
|
|
1765
|
+
const routeHash = route(model, options);
|
|
1766
|
+
const policyHash = digest(requestPolicy(model, prepared));
|
|
1767
|
+
const latest = latestCompaction(branch);
|
|
1768
|
+
let result = payload;
|
|
1769
|
+
if (active && latest?.id === active.entryId && latest.summary === active.summary && digest(latest.details) === active.detailsHash && unchanged(branch, active.branchLength, active.branchHash) && active.routeHash === routeHash && active.policyHash === policyHash) {
|
|
1770
|
+
const details = readNativeCompactionDetails(latest.details);
|
|
1771
|
+
const projection = owner.sessionManager.buildSessionProjection();
|
|
1772
|
+
const contribution = projection.entries.find((entry) => entry.sourceEntry.id === latest.id);
|
|
1773
|
+
const prefix = contribution ? transcript(contribution.messages) : undefined;
|
|
1774
|
+
const slot = prefix ? summarySlot(model, prepared, prefix) : undefined;
|
|
1775
|
+
if (details?.version === 3 && details.coverage?.runtimeId === runtimeId && details.coverage.firstKeptEntryId === latest.firstKeptEntryId && slot !== undefined) {
|
|
1776
|
+
const replaced = substituteSummary(payload, slot, prepared.portableInput[slot], details.output);
|
|
1777
|
+
if (replaced) {
|
|
1778
|
+
result = replaced;
|
|
1779
|
+
prepared.payload = structuredClone(replaced);
|
|
1780
|
+
prepared.replacement = { index: slot, count: details.output.length };
|
|
1781
|
+
} else
|
|
1782
|
+
active = undefined;
|
|
1783
|
+
} else
|
|
1784
|
+
active = undefined;
|
|
1785
|
+
} else
|
|
1786
|
+
active = undefined;
|
|
1787
|
+
captureStatus = "eligible prepared request captured";
|
|
1788
|
+
snapshot = {
|
|
1789
|
+
prepared,
|
|
1790
|
+
model: structuredClone(model),
|
|
1791
|
+
provider,
|
|
1792
|
+
options: { apiKey: options.apiKey, headers: structuredClone(options.headers), env: structuredClone(options.env), fetch: options.fetch },
|
|
1793
|
+
branchLength: branch.length,
|
|
1794
|
+
branchHash,
|
|
1795
|
+
routeHash,
|
|
1796
|
+
authHash: routeDigest(auth),
|
|
1797
|
+
policyHash,
|
|
1798
|
+
sessionId: owner.sessionManager.getSessionId()
|
|
1799
|
+
};
|
|
1800
|
+
return result;
|
|
1801
|
+
};
|
|
1802
|
+
};
|
|
1803
|
+
const install = (context) => {
|
|
1804
|
+
ctx = context;
|
|
1805
|
+
if (installedProvider === context.model?.provider)
|
|
1806
|
+
return;
|
|
1807
|
+
uninstall?.();
|
|
1808
|
+
uninstall = undefined;
|
|
1809
|
+
installedProvider = undefined;
|
|
1810
|
+
if (!supportsNativeCompaction(context.model))
|
|
1811
|
+
return;
|
|
1812
|
+
uninstall = installObserver(pi, context, observe);
|
|
1813
|
+
if (uninstall)
|
|
1814
|
+
installedProvider = context.model.provider;
|
|
1815
|
+
captureStatus = uninstall ? "awaiting prepared request" : "unsupported provider registration";
|
|
1816
|
+
};
|
|
1817
|
+
pi.on("session_start", (_event, context) => {
|
|
1818
|
+
invalidate();
|
|
1819
|
+
install(context);
|
|
1820
|
+
const latest = latestCompaction(context.sessionManager.getBranch());
|
|
1821
|
+
if (latest?.summary === NATIVE_COMPACTION_SUMMARY)
|
|
1822
|
+
context.ui.notify("Legacy native compaction has no portable summary. Native replay is disabled; recover from earlier history or start a new session.", "warning");
|
|
1823
|
+
});
|
|
1824
|
+
pi.on("before_agent_start", (_event, context) => {
|
|
1825
|
+
install(context);
|
|
1826
|
+
});
|
|
1827
|
+
pi.on("model_select", (_event, context) => {
|
|
1828
|
+
invalidate();
|
|
1829
|
+
install(context);
|
|
1830
|
+
});
|
|
1831
|
+
pi.on("session_tree", () => {
|
|
1832
|
+
invalidate();
|
|
1833
|
+
});
|
|
1834
|
+
pi.on("session_shutdown", () => {
|
|
1835
|
+
invalidate();
|
|
1836
|
+
uninstall?.();
|
|
1837
|
+
uninstall = undefined;
|
|
1838
|
+
installedProvider = undefined;
|
|
1839
|
+
ctx = undefined;
|
|
1840
|
+
});
|
|
1841
|
+
const removeInvalidation = pi.events.on("pi-provider-compaction:invalidate", invalidate);
|
|
1842
|
+
pi.on("session_shutdown", () => {
|
|
1843
|
+
if (typeof removeInvalidation === "function")
|
|
1844
|
+
removeInvalidation();
|
|
1845
|
+
});
|
|
1846
|
+
pi.on("session_before_compact", async (event, context) => {
|
|
1847
|
+
const capture = snapshot;
|
|
1848
|
+
snapshot = undefined;
|
|
1849
|
+
pending = undefined;
|
|
1850
|
+
if (!capture) {
|
|
1851
|
+
fallback(`no eligible snapshot (${captureStatus})`);
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
if (!context.model || generic(context) || event.signal.aborted) {
|
|
1855
|
+
fallback("missing model, generic model override, or aborted attempt");
|
|
1856
|
+
return;
|
|
1857
|
+
}
|
|
1858
|
+
const protocol = resolveNativeProtocol(context.model);
|
|
1859
|
+
if (!protocol || context.sessionManager.getSessionId() !== capture.sessionId) {
|
|
1860
|
+
fallback("unsupported protocol or changed session");
|
|
1861
|
+
return;
|
|
1862
|
+
}
|
|
1863
|
+
const ticket = generation;
|
|
1864
|
+
const branch = context.sessionManager.getBranch();
|
|
1865
|
+
const branchHash = digest(branch);
|
|
1866
|
+
if (!equal(branch, event.branchEntries) || !unchanged(branch, capture.branchLength, capture.branchHash)) {
|
|
1867
|
+
fallback("history changed since capture");
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
const projection = context.sessionManager.buildSessionProjection();
|
|
1871
|
+
const boundary = projection.entries.findIndex((entry) => entry.sourceEntry.id === event.preparation.firstKeptEntryId);
|
|
1872
|
+
if (boundary < 0) {
|
|
1873
|
+
fallback("retained boundary missing from projection");
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
const prefix = transcript(projection.entries.slice(0, boundary).flatMap((entry) => entry.messages));
|
|
1877
|
+
const input = selectCoveredPrefix(capture.model, capture.prepared, prefix, fallback);
|
|
1878
|
+
if (!input)
|
|
1879
|
+
return;
|
|
1880
|
+
const current = () => ticket === generation && !event.signal.aborted && supportedRouteOptions(capture.model, capture.options) && !generic(context) && context.modelRegistry.getProvider(capture.model.provider) === capture.provider && digest(context.sessionManager.getBranch()) === branchHash;
|
|
1881
|
+
const auth = await context.modelRegistry.getApiKeyAndHeaders(context.model);
|
|
1882
|
+
if (!current())
|
|
1883
|
+
return { cancel: true };
|
|
1884
|
+
if (!auth.ok || routeDigest(auth) !== capture.authHash) {
|
|
1885
|
+
fallback("auth changed since capture");
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
compactionStatus = "generating portable summary";
|
|
1889
|
+
let portable;
|
|
1890
|
+
try {
|
|
1891
|
+
portable = await compact(event.preparation, capture.model, auth.apiKey, auth.headers, event.customInstructions, event.signal, context.thinkingLevel, capture.provider.streamSimple.bind(capture.provider), auth.env, undefined, undefined, capture.sessionId);
|
|
1892
|
+
} catch {
|
|
1893
|
+
return !current() ? { cancel: true } : undefined;
|
|
1894
|
+
}
|
|
1895
|
+
if (!current() || !portable.summary.trim() || portable.summary === NATIVE_COMPACTION_SUMMARY)
|
|
1896
|
+
return { cancel: true };
|
|
1897
|
+
try {
|
|
1898
|
+
const freshAuth = await context.modelRegistry.getApiKeyAndHeaders(context.model);
|
|
1899
|
+
if (!current())
|
|
1900
|
+
return { cancel: true };
|
|
1901
|
+
if (!freshAuth.ok || routeDigest(freshAuth) !== capture.authHash) {
|
|
1902
|
+
fallback("auth changed after portable generation");
|
|
1903
|
+
return { compaction: portable };
|
|
1904
|
+
}
|
|
1905
|
+
compactionStatus = "requesting native checkpoint";
|
|
1906
|
+
const native = await requestProviderCompaction({
|
|
1907
|
+
provider: capture.provider,
|
|
1908
|
+
model: capture.model,
|
|
1909
|
+
context: capture.prepared.context,
|
|
1910
|
+
input,
|
|
1911
|
+
preparedPayload: capture.prepared.payload,
|
|
1912
|
+
protocol,
|
|
1913
|
+
apiKey: capture.options.apiKey,
|
|
1914
|
+
headers: capture.options.headers,
|
|
1915
|
+
env: capture.options.env,
|
|
1916
|
+
signal: event.signal,
|
|
1917
|
+
fetch: capture.options.fetch
|
|
1918
|
+
});
|
|
1919
|
+
if (!current())
|
|
1920
|
+
return { cancel: true };
|
|
1921
|
+
const finalAuth = await context.modelRegistry.getApiKeyAndHeaders(context.model);
|
|
1922
|
+
if (!current())
|
|
1923
|
+
return { cancel: true };
|
|
1924
|
+
if (!finalAuth.ok || routeDigest(finalAuth) !== capture.authHash) {
|
|
1925
|
+
fallback("auth changed after native generation");
|
|
1926
|
+
return { compaction: portable };
|
|
1927
|
+
}
|
|
1928
|
+
const details = {
|
|
1929
|
+
...isObject(portable.details) ? portable.details : {},
|
|
1930
|
+
type: NATIVE_COMPACTION_TYPE,
|
|
1931
|
+
version: 3,
|
|
1932
|
+
provider: capture.model.provider,
|
|
1933
|
+
api: capture.model.api,
|
|
1934
|
+
model: capture.model.id,
|
|
1935
|
+
protocol,
|
|
1936
|
+
identity: identity(capture.model, capture.options),
|
|
1937
|
+
output: native.output,
|
|
1938
|
+
nativeUsage: native.usage,
|
|
1939
|
+
portableUsage: portable.usage,
|
|
1940
|
+
coverage: { kind: "summarized-prefix", firstKeptEntryId: portable.firstKeptEntryId, sourceLeafId: branch.at(-1).id, sourceHash: branchHash, policyHash: capture.policyHash, inputHash: digest(input), runtimeId }
|
|
1941
|
+
};
|
|
1942
|
+
pending = { details, capture, summary: portable.summary };
|
|
1943
|
+
return { compaction: { ...portable, usage: combineUsage(native.usage, portable.usage), details } };
|
|
1944
|
+
} catch {
|
|
1945
|
+
if (!current())
|
|
1946
|
+
return { cancel: true };
|
|
1947
|
+
fallback("native acquisition or validation failed");
|
|
1948
|
+
return { compaction: portable };
|
|
1949
|
+
}
|
|
1950
|
+
});
|
|
1951
|
+
pi.on("session_compact", (event, context) => {
|
|
1952
|
+
if (readNativeCompactionDetails(event.compactionEntry.details)?.version === 3)
|
|
1953
|
+
compactionStatus = "native checkpoint committed";
|
|
1954
|
+
else if (!compactionStatus.startsWith("portable:"))
|
|
1955
|
+
fallback("another handler supplied the committed result");
|
|
1956
|
+
if (compactionStatus.startsWith("portable:"))
|
|
1957
|
+
pi.appendEntry("pi-provider-compaction:diagnostic", { status: compactionStatus });
|
|
1958
|
+
active = undefined;
|
|
1959
|
+
snapshot = undefined;
|
|
1960
|
+
const candidate = pending;
|
|
1961
|
+
pending = undefined;
|
|
1962
|
+
if (!candidate || !equal(event.compactionEntry.details, candidate.details) || event.compactionEntry.summary !== candidate.summary)
|
|
1963
|
+
return;
|
|
1964
|
+
const branch = context.sessionManager.getBranch();
|
|
1965
|
+
const coverage = candidate.details.coverage;
|
|
1966
|
+
if (!coverage || event.compactionEntry.firstKeptEntryId !== coverage.firstKeptEntryId || event.compactionEntry.parentId !== coverage.sourceLeafId || branch.at(-1)?.id !== event.compactionEntry.id || digest(branch.slice(0, -1)) !== coverage.sourceHash)
|
|
1967
|
+
return;
|
|
1968
|
+
active = { entryId: event.compactionEntry.id, detailsHash: digest(candidate.details), summary: candidate.summary, branchLength: branch.length, branchHash: digest(branch), routeHash: candidate.capture.routeHash, policyHash: candidate.capture.policyHash };
|
|
1969
|
+
});
|
|
1970
|
+
pi.on("session_compact_failed", () => {
|
|
1971
|
+
pending = undefined;
|
|
1972
|
+
snapshot = undefined;
|
|
1973
|
+
compactionStatus = "compaction failed or cancelled";
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
export {
|
|
1977
|
+
NATIVE_COMPACTION_SUMMARY,
|
|
1978
|
+
NATIVE_COMPACTION_TYPE,
|
|
1979
|
+
NATIVE_COMPACTION_VERSION,
|
|
1980
|
+
pi_provider_compaction_default as default,
|
|
1981
|
+
readNativeCompactionDetails,
|
|
1982
|
+
supportsNativeCompaction
|
|
1983
|
+
};
|