pi-openai-toolkit 0.3.0
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/LICENSE +21 -0
- package/NOTICE +33 -0
- package/README.md +175 -0
- package/README_zh.md +172 -0
- package/extensions/compaction.ts +1 -0
- package/extensions/web-search.ts +1 -0
- package/package.json +79 -0
- package/src/compact-client.ts +426 -0
- package/src/config.ts +273 -0
- package/src/debug.ts +168 -0
- package/src/details-store.ts +151 -0
- package/src/extension-runtime.ts +537 -0
- package/src/native-fallback.ts +151 -0
- package/src/payload-rewrite.ts +556 -0
- package/src/provider-headers.ts +23 -0
- package/src/remote-v2-client.ts +526 -0
- package/src/request-context-cache.ts +106 -0
- package/src/runtime.ts +283 -0
- package/src/serializer.ts +554 -0
- package/src/supported-environment.ts +17 -0
- package/src/types.ts +333 -0
- package/src/web-search/extension.ts +80 -0
- package/src/web-search/payload.ts +96 -0
- package/src/web-search/prompt.ts +20 -0
- package/src/web-search/types.ts +47 -0
- package/tsconfig.check.json +36 -0
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
import { writeDebugArtifact } from "./debug";
|
|
2
|
+
import { mergeProviderHeaders } from "./provider-headers";
|
|
3
|
+
import type { NativeCompactionRuntime } from "./runtime";
|
|
4
|
+
import type { NativeCompactionRequestBody, ResponsesInputItem } from "./serializer";
|
|
5
|
+
import type { ArtifactContext, CompactionConfig } from "./types";
|
|
6
|
+
|
|
7
|
+
const JSON_CONTENT_TYPE = "application/json";
|
|
8
|
+
const SSE_CONTENT_TYPE = "text/event-stream";
|
|
9
|
+
|
|
10
|
+
export type RemoteV2CompactionItem = Record<string, unknown> & {
|
|
11
|
+
type: "compaction";
|
|
12
|
+
encrypted_content: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type RemoteV2ResponseUsage = {
|
|
16
|
+
input_tokens?: number;
|
|
17
|
+
output_tokens?: number;
|
|
18
|
+
total_tokens?: number;
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type RemoteV2ResponseEnvelope = Record<string, unknown> & {
|
|
23
|
+
id?: string;
|
|
24
|
+
created_at?: number | string;
|
|
25
|
+
status: "completed";
|
|
26
|
+
output: RemoteV2CompactionItem[];
|
|
27
|
+
usage?: RemoteV2ResponseUsage;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type RemoteV2CompactionClientFailureReason =
|
|
31
|
+
| "aborted"
|
|
32
|
+
| "network-error"
|
|
33
|
+
| "non-2xx"
|
|
34
|
+
| "empty-body"
|
|
35
|
+
| "invalid-sse"
|
|
36
|
+
| "error-event"
|
|
37
|
+
| "missing-completed-event"
|
|
38
|
+
| "incomplete-response"
|
|
39
|
+
| "invalid-compaction-count"
|
|
40
|
+
| "malformed-compaction-item";
|
|
41
|
+
|
|
42
|
+
export type RemoteV2CompactionClientSuccess = {
|
|
43
|
+
ok: true;
|
|
44
|
+
status: number;
|
|
45
|
+
compactedWindow: [RemoteV2CompactionItem];
|
|
46
|
+
compactResponseId?: string;
|
|
47
|
+
createdAt?: string;
|
|
48
|
+
usage?: RemoteV2ResponseUsage;
|
|
49
|
+
response: RemoteV2ResponseEnvelope;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type RemoteV2CompactionClientFailure = {
|
|
53
|
+
ok: false;
|
|
54
|
+
reason: RemoteV2CompactionClientFailureReason;
|
|
55
|
+
status?: number;
|
|
56
|
+
errorMessage?: string;
|
|
57
|
+
responseText?: string;
|
|
58
|
+
responseJson?: unknown;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type RemoteV2CompactionClientResult = RemoteV2CompactionClientSuccess | RemoteV2CompactionClientFailure;
|
|
62
|
+
|
|
63
|
+
export type RemoteV2CompactionRequestBody = Omit<NativeCompactionRequestBody, "input"> & {
|
|
64
|
+
input: [...ResponsesInputItem[], { type: "compaction_trigger" }];
|
|
65
|
+
store: false;
|
|
66
|
+
stream: true;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type ExecuteRemoteV2CompactionOptions = {
|
|
70
|
+
runtime: NativeCompactionRuntime;
|
|
71
|
+
request: NativeCompactionRequestBody;
|
|
72
|
+
signal?: AbortSignal;
|
|
73
|
+
settings?: CompactionConfig;
|
|
74
|
+
context?: ArtifactContext;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
type ParsedSseEvent = {
|
|
78
|
+
event?: string;
|
|
79
|
+
dataText: string;
|
|
80
|
+
data?: unknown;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
84
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isAbortError(error: unknown): boolean {
|
|
88
|
+
return (
|
|
89
|
+
(error instanceof DOMException && error.name === "AbortError") ||
|
|
90
|
+
(error instanceof Error && (error.name === "AbortError" || error.name === "ABORT_ERR"))
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isNonEmptyString(value: unknown): value is string {
|
|
95
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function normalizeResponseTimestamp(value: unknown): string | undefined {
|
|
99
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
100
|
+
const milliseconds = value > 1_000_000_000_000 ? value : value * 1000;
|
|
101
|
+
return new Date(milliseconds).toISOString();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (typeof value !== "string") {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const trimmed = value.trim();
|
|
109
|
+
if (!trimmed) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const parsed = Date.parse(trimmed);
|
|
114
|
+
return Number.isNaN(parsed) ? trimmed : new Date(parsed).toISOString();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function decodeJwtPayload(token: string): Record<string, unknown> | undefined {
|
|
118
|
+
const parts = token.split(".");
|
|
119
|
+
if (parts.length !== 3) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const payloadText = Buffer.from(parts[1]!, "base64url").toString("utf8");
|
|
125
|
+
const payload = JSON.parse(payloadText);
|
|
126
|
+
return isRecord(payload) ? payload : undefined;
|
|
127
|
+
} catch {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function extractCodexAccountId(token: string): string | undefined {
|
|
133
|
+
const payload = decodeJwtPayload(token);
|
|
134
|
+
const authClaims = payload?.["https://api.openai.com/auth"];
|
|
135
|
+
if (!isRecord(authClaims)) {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const accountId = authClaims.chatgpt_account_id;
|
|
140
|
+
return isNonEmptyString(accountId) ? accountId.trim() : undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function buildCodexUserAgent(): string {
|
|
144
|
+
const platform = typeof process !== "undefined" ? process.platform : "browser";
|
|
145
|
+
const arch = typeof process !== "undefined" ? process.arch : "unknown";
|
|
146
|
+
return `pi (${platform}; ${arch})`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function toHeaders(runtime: NativeCompactionRuntime): Record<string, string> {
|
|
150
|
+
const headers = new Headers(mergeProviderHeaders(runtime.currentModel.headers, runtime.headers));
|
|
151
|
+
headers.set("accept", SSE_CONTENT_TYPE);
|
|
152
|
+
headers.set("content-type", JSON_CONTENT_TYPE);
|
|
153
|
+
if (!headers.has("authorization")) {
|
|
154
|
+
headers.set("authorization", `Bearer ${runtime.apiKey}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (runtime.api === "openai-codex-responses") {
|
|
158
|
+
const accountId = extractCodexAccountId(runtime.apiKey);
|
|
159
|
+
if (accountId) {
|
|
160
|
+
headers.set("chatgpt-account-id", accountId);
|
|
161
|
+
}
|
|
162
|
+
headers.set("originator", "pi");
|
|
163
|
+
headers.set("user-agent", buildCodexUserAgent());
|
|
164
|
+
headers.set("openai-beta", "responses=experimental");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return Object.fromEntries(headers.entries());
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function buildRemoteV2CompactionRequest(
|
|
171
|
+
request: NativeCompactionRequestBody,
|
|
172
|
+
): RemoteV2CompactionRequestBody {
|
|
173
|
+
return {
|
|
174
|
+
...structuredClone(request),
|
|
175
|
+
input: [...structuredClone(request.input), { type: "compaction_trigger" }],
|
|
176
|
+
store: false,
|
|
177
|
+
stream: true,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function parseSseEvents(raw: string): ParsedSseEvent[] | undefined {
|
|
182
|
+
const normalized = raw.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
183
|
+
const events: ParsedSseEvent[] = [];
|
|
184
|
+
|
|
185
|
+
for (const block of normalized.split(/\n\n+/)) {
|
|
186
|
+
if (!block.trim()) continue;
|
|
187
|
+
|
|
188
|
+
let event: string | undefined;
|
|
189
|
+
const dataLines: string[] = [];
|
|
190
|
+
for (const line of block.split("\n")) {
|
|
191
|
+
if (line.startsWith(":")) continue;
|
|
192
|
+
if (line.startsWith("event:")) {
|
|
193
|
+
event = line.slice("event:".length).trim();
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (line.startsWith("data:")) {
|
|
197
|
+
dataLines.push(line.slice("data:".length).replace(/^ /, ""));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (dataLines.length === 0) continue;
|
|
202
|
+
const dataText = dataLines.join("\n");
|
|
203
|
+
if (dataText === "[DONE]") {
|
|
204
|
+
events.push({ event, dataText });
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
events.push({ event, dataText, data: JSON.parse(dataText) });
|
|
210
|
+
} catch {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return events.length > 0 ? events : undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function isRemoteV2CompactionItem(value: unknown): value is RemoteV2CompactionItem {
|
|
219
|
+
return isRecord(value) && value.type === "compaction" && isNonEmptyString(value.encrypted_content);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function getEventType(event: ParsedSseEvent): string | undefined {
|
|
223
|
+
return isRecord(event.data) && isNonEmptyString(event.data.type) ? event.data.type : event.event;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function getErrorMessage(value: unknown): string | undefined {
|
|
227
|
+
if (!isRecord(value)) return undefined;
|
|
228
|
+
if (isNonEmptyString(value.message)) return value.message;
|
|
229
|
+
if (isRecord(value.error) && isNonEmptyString(value.error.message)) return value.error.message;
|
|
230
|
+
if (isNonEmptyString(value.error)) return value.error;
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function writeCompactArtifact(
|
|
235
|
+
data: unknown,
|
|
236
|
+
settings: CompactionConfig | undefined,
|
|
237
|
+
context: ArtifactContext | undefined,
|
|
238
|
+
): void {
|
|
239
|
+
if (!settings || !context) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
writeDebugArtifact("compact-response", data, settings, context);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function summarizeEvents(events: readonly ParsedSseEvent[]): unknown[] {
|
|
247
|
+
return events.map((event) => {
|
|
248
|
+
const data = isRecord(event.data) ? event.data : undefined;
|
|
249
|
+
const response = data && isRecord(data.response) ? data.response : undefined;
|
|
250
|
+
const item = data && isRecord(data.item) ? data.item : undefined;
|
|
251
|
+
return {
|
|
252
|
+
event: event.event,
|
|
253
|
+
type: data?.type,
|
|
254
|
+
responseStatus: response?.status,
|
|
255
|
+
responseId: response?.id,
|
|
256
|
+
itemType: item?.type,
|
|
257
|
+
};
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export async function executeRemoteV2Compaction(
|
|
262
|
+
options: ExecuteRemoteV2CompactionOptions,
|
|
263
|
+
): Promise<RemoteV2CompactionClientResult> {
|
|
264
|
+
const { runtime, signal, settings, context } = options;
|
|
265
|
+
const request = buildRemoteV2CompactionRequest(options.request);
|
|
266
|
+
const headers = toHeaders(runtime);
|
|
267
|
+
|
|
268
|
+
if (signal?.aborted) {
|
|
269
|
+
const aborted: RemoteV2CompactionClientFailure = { ok: false, reason: "aborted" };
|
|
270
|
+
writeCompactArtifact(
|
|
271
|
+
{
|
|
272
|
+
protocol: "remote_compaction_v2",
|
|
273
|
+
request: { url: runtime.responsesUrl, headers, body: request },
|
|
274
|
+
outcome: aborted,
|
|
275
|
+
},
|
|
276
|
+
settings,
|
|
277
|
+
context,
|
|
278
|
+
);
|
|
279
|
+
return aborted;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
const response = await fetch(runtime.responsesUrl, {
|
|
284
|
+
method: "POST",
|
|
285
|
+
headers,
|
|
286
|
+
body: JSON.stringify(request),
|
|
287
|
+
signal,
|
|
288
|
+
});
|
|
289
|
+
const responseText = await response.text();
|
|
290
|
+
const responseHeaders: Record<string, string> = {};
|
|
291
|
+
response.headers.forEach((value, key) => {
|
|
292
|
+
responseHeaders[key] = value;
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
if (!response.ok) {
|
|
296
|
+
let responseJson: unknown;
|
|
297
|
+
if (responseText.trim()) {
|
|
298
|
+
try {
|
|
299
|
+
responseJson = JSON.parse(responseText);
|
|
300
|
+
} catch {
|
|
301
|
+
responseJson = undefined;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const failure: RemoteV2CompactionClientFailure = {
|
|
305
|
+
ok: false,
|
|
306
|
+
reason: "non-2xx",
|
|
307
|
+
status: response.status,
|
|
308
|
+
responseText: responseText || undefined,
|
|
309
|
+
responseJson,
|
|
310
|
+
};
|
|
311
|
+
writeCompactArtifact(
|
|
312
|
+
{
|
|
313
|
+
protocol: "remote_compaction_v2",
|
|
314
|
+
request: { url: runtime.responsesUrl, headers, body: request },
|
|
315
|
+
response: { status: response.status, headers: responseHeaders, body: responseJson ?? responseText },
|
|
316
|
+
outcome: failure,
|
|
317
|
+
},
|
|
318
|
+
settings,
|
|
319
|
+
context,
|
|
320
|
+
);
|
|
321
|
+
return failure;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (!responseText.trim()) {
|
|
325
|
+
const failure: RemoteV2CompactionClientFailure = {
|
|
326
|
+
ok: false,
|
|
327
|
+
reason: "empty-body",
|
|
328
|
+
status: response.status,
|
|
329
|
+
};
|
|
330
|
+
writeCompactArtifact(
|
|
331
|
+
{
|
|
332
|
+
protocol: "remote_compaction_v2",
|
|
333
|
+
request: { url: runtime.responsesUrl, headers, body: request },
|
|
334
|
+
response: { status: response.status, headers: responseHeaders },
|
|
335
|
+
outcome: failure,
|
|
336
|
+
},
|
|
337
|
+
settings,
|
|
338
|
+
context,
|
|
339
|
+
);
|
|
340
|
+
return failure;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const events = parseSseEvents(responseText);
|
|
344
|
+
if (!events) {
|
|
345
|
+
const failure: RemoteV2CompactionClientFailure = {
|
|
346
|
+
ok: false,
|
|
347
|
+
reason: "invalid-sse",
|
|
348
|
+
status: response.status,
|
|
349
|
+
responseText,
|
|
350
|
+
};
|
|
351
|
+
writeCompactArtifact(
|
|
352
|
+
{
|
|
353
|
+
protocol: "remote_compaction_v2",
|
|
354
|
+
request: { url: runtime.responsesUrl, headers, body: request },
|
|
355
|
+
response: { status: response.status, headers: responseHeaders, body: responseText },
|
|
356
|
+
outcome: failure,
|
|
357
|
+
},
|
|
358
|
+
settings,
|
|
359
|
+
context,
|
|
360
|
+
);
|
|
361
|
+
return failure;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const errorEvent = events.find((event) => {
|
|
365
|
+
const type = getEventType(event);
|
|
366
|
+
return type === "error" || type === "response.failed" || type === "response.incomplete";
|
|
367
|
+
});
|
|
368
|
+
if (errorEvent) {
|
|
369
|
+
const failure: RemoteV2CompactionClientFailure = {
|
|
370
|
+
ok: false,
|
|
371
|
+
reason: "error-event",
|
|
372
|
+
status: response.status,
|
|
373
|
+
errorMessage: getErrorMessage(errorEvent.data),
|
|
374
|
+
responseJson: errorEvent.data,
|
|
375
|
+
};
|
|
376
|
+
writeCompactArtifact(
|
|
377
|
+
{
|
|
378
|
+
protocol: "remote_compaction_v2",
|
|
379
|
+
request: { url: runtime.responsesUrl, headers, body: request },
|
|
380
|
+
response: { status: response.status, headers: responseHeaders, events: summarizeEvents(events) },
|
|
381
|
+
outcome: failure,
|
|
382
|
+
},
|
|
383
|
+
settings,
|
|
384
|
+
context,
|
|
385
|
+
);
|
|
386
|
+
return failure;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const completedEvent = events.find((event) => getEventType(event) === "response.completed");
|
|
390
|
+
const completedData = completedEvent && isRecord(completedEvent.data) ? completedEvent.data : undefined;
|
|
391
|
+
const completedResponse = completedData && isRecord(completedData.response) ? completedData.response : undefined;
|
|
392
|
+
if (!completedResponse) {
|
|
393
|
+
const failure: RemoteV2CompactionClientFailure = {
|
|
394
|
+
ok: false,
|
|
395
|
+
reason: "missing-completed-event",
|
|
396
|
+
status: response.status,
|
|
397
|
+
};
|
|
398
|
+
writeCompactArtifact(
|
|
399
|
+
{
|
|
400
|
+
protocol: "remote_compaction_v2",
|
|
401
|
+
request: { url: runtime.responsesUrl, headers, body: request },
|
|
402
|
+
response: { status: response.status, headers: responseHeaders, events: summarizeEvents(events) },
|
|
403
|
+
outcome: failure,
|
|
404
|
+
},
|
|
405
|
+
settings,
|
|
406
|
+
context,
|
|
407
|
+
);
|
|
408
|
+
return failure;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (completedResponse.status !== "completed" || !Array.isArray(completedResponse.output)) {
|
|
412
|
+
const failure: RemoteV2CompactionClientFailure = {
|
|
413
|
+
ok: false,
|
|
414
|
+
reason: "incomplete-response",
|
|
415
|
+
status: response.status,
|
|
416
|
+
responseJson: completedResponse,
|
|
417
|
+
};
|
|
418
|
+
writeCompactArtifact(
|
|
419
|
+
{
|
|
420
|
+
protocol: "remote_compaction_v2",
|
|
421
|
+
request: { url: runtime.responsesUrl, headers, body: request },
|
|
422
|
+
response: { status: response.status, headers: responseHeaders, events: summarizeEvents(events) },
|
|
423
|
+
outcome: failure,
|
|
424
|
+
},
|
|
425
|
+
settings,
|
|
426
|
+
context,
|
|
427
|
+
);
|
|
428
|
+
return failure;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const compactionItems = completedResponse.output.filter(
|
|
432
|
+
(item): item is Record<string, unknown> => isRecord(item) && item.type === "compaction",
|
|
433
|
+
);
|
|
434
|
+
if (compactionItems.length !== 1) {
|
|
435
|
+
const failure: RemoteV2CompactionClientFailure = {
|
|
436
|
+
ok: false,
|
|
437
|
+
reason: "invalid-compaction-count",
|
|
438
|
+
status: response.status,
|
|
439
|
+
responseJson: completedResponse,
|
|
440
|
+
};
|
|
441
|
+
writeCompactArtifact(
|
|
442
|
+
{
|
|
443
|
+
protocol: "remote_compaction_v2",
|
|
444
|
+
request: { url: runtime.responsesUrl, headers, body: request },
|
|
445
|
+
response: { status: response.status, headers: responseHeaders, events: summarizeEvents(events) },
|
|
446
|
+
outcome: failure,
|
|
447
|
+
},
|
|
448
|
+
settings,
|
|
449
|
+
context,
|
|
450
|
+
);
|
|
451
|
+
return failure;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const compactionItem = compactionItems[0];
|
|
455
|
+
if (!isRemoteV2CompactionItem(compactionItem)) {
|
|
456
|
+
const failure: RemoteV2CompactionClientFailure = {
|
|
457
|
+
ok: false,
|
|
458
|
+
reason: "malformed-compaction-item",
|
|
459
|
+
status: response.status,
|
|
460
|
+
responseJson: compactionItem,
|
|
461
|
+
};
|
|
462
|
+
writeCompactArtifact(
|
|
463
|
+
{
|
|
464
|
+
protocol: "remote_compaction_v2",
|
|
465
|
+
request: { url: runtime.responsesUrl, headers, body: request },
|
|
466
|
+
response: { status: response.status, headers: responseHeaders, events: summarizeEvents(events) },
|
|
467
|
+
outcome: failure,
|
|
468
|
+
},
|
|
469
|
+
settings,
|
|
470
|
+
context,
|
|
471
|
+
);
|
|
472
|
+
return failure;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const responseEnvelope = completedResponse as RemoteV2ResponseEnvelope;
|
|
476
|
+
const success: RemoteV2CompactionClientSuccess = {
|
|
477
|
+
ok: true,
|
|
478
|
+
status: response.status,
|
|
479
|
+
compactedWindow: [structuredClone(compactionItem)],
|
|
480
|
+
compactResponseId: isNonEmptyString(responseEnvelope.id) ? responseEnvelope.id.trim() : undefined,
|
|
481
|
+
createdAt: normalizeResponseTimestamp(responseEnvelope.created_at),
|
|
482
|
+
usage: isRecord(responseEnvelope.usage) ? (structuredClone(responseEnvelope.usage) as RemoteV2ResponseUsage) : undefined,
|
|
483
|
+
response: structuredClone(responseEnvelope),
|
|
484
|
+
};
|
|
485
|
+
writeCompactArtifact(
|
|
486
|
+
{
|
|
487
|
+
protocol: "remote_compaction_v2",
|
|
488
|
+
request: { url: runtime.responsesUrl, headers, body: request },
|
|
489
|
+
response: {
|
|
490
|
+
status: response.status,
|
|
491
|
+
headers: responseHeaders,
|
|
492
|
+
events: summarizeEvents(events),
|
|
493
|
+
completed: responseEnvelope,
|
|
494
|
+
},
|
|
495
|
+
outcome: {
|
|
496
|
+
ok: true,
|
|
497
|
+
status: success.status,
|
|
498
|
+
compactResponseId: success.compactResponseId,
|
|
499
|
+
createdAt: success.createdAt,
|
|
500
|
+
compactedItems: 1,
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
settings,
|
|
504
|
+
context,
|
|
505
|
+
);
|
|
506
|
+
return success;
|
|
507
|
+
} catch (error) {
|
|
508
|
+
const failure: RemoteV2CompactionClientFailure = isAbortError(error)
|
|
509
|
+
? { ok: false, reason: "aborted" }
|
|
510
|
+
: {
|
|
511
|
+
ok: false,
|
|
512
|
+
reason: "network-error",
|
|
513
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
514
|
+
};
|
|
515
|
+
writeCompactArtifact(
|
|
516
|
+
{
|
|
517
|
+
protocol: "remote_compaction_v2",
|
|
518
|
+
request: { url: runtime.responsesUrl, headers, body: request },
|
|
519
|
+
outcome: failure,
|
|
520
|
+
},
|
|
521
|
+
settings,
|
|
522
|
+
context,
|
|
523
|
+
);
|
|
524
|
+
return failure;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { ResponsesCompatibleRequestPayload } from "./runtime";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Explicit allowlist of fields mirrored from the latest live Responses request.
|
|
5
|
+
* remote_compaction_v2 has no provider payload at session_before_compact time, so
|
|
6
|
+
* the extension reuses only these compact-relevant fields for the same runtime identity.
|
|
7
|
+
* `stream`, `store`, `input`, and trigger semantics are always forced separately.
|
|
8
|
+
*/
|
|
9
|
+
export type CompactionRequestExtras = {
|
|
10
|
+
tools?: unknown[];
|
|
11
|
+
parallel_tool_calls?: boolean;
|
|
12
|
+
reasoning?: Record<string, unknown>;
|
|
13
|
+
service_tier?: string;
|
|
14
|
+
prompt_cache_key?: string;
|
|
15
|
+
text?: Record<string, unknown>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type RequestContextIdentity = {
|
|
19
|
+
provider: string;
|
|
20
|
+
api: string;
|
|
21
|
+
model: string;
|
|
22
|
+
baseUrl: string;
|
|
23
|
+
sessionId?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type CachedRequestContext = {
|
|
27
|
+
identity: RequestContextIdentity;
|
|
28
|
+
extras: CompactionRequestExtras;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
let cached: CachedRequestContext | undefined;
|
|
32
|
+
|
|
33
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
34
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function sameIdentity(cachedIdentity: RequestContextIdentity, current: RequestContextIdentity): boolean {
|
|
38
|
+
return (
|
|
39
|
+
cachedIdentity.provider === current.provider &&
|
|
40
|
+
cachedIdentity.api === current.api &&
|
|
41
|
+
cachedIdentity.model === current.model &&
|
|
42
|
+
cachedIdentity.baseUrl === current.baseUrl &&
|
|
43
|
+
cachedIdentity.sessionId === current.sessionId
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Remember compact-relevant fields from a live Responses request payload.
|
|
49
|
+
* Purely additive metadata: failures are swallowed so caching can never
|
|
50
|
+
* break the provider request path.
|
|
51
|
+
*/
|
|
52
|
+
export function rememberRequestContext(
|
|
53
|
+
payload: ResponsesCompatibleRequestPayload,
|
|
54
|
+
identity: RequestContextIdentity,
|
|
55
|
+
): void {
|
|
56
|
+
try {
|
|
57
|
+
if (payload.model !== identity.model) {
|
|
58
|
+
cached = undefined;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const extras: CompactionRequestExtras = {};
|
|
63
|
+
if (Array.isArray(payload.tools)) {
|
|
64
|
+
extras.tools = structuredClone(payload.tools);
|
|
65
|
+
}
|
|
66
|
+
if (typeof payload.parallel_tool_calls === "boolean") {
|
|
67
|
+
extras.parallel_tool_calls = payload.parallel_tool_calls;
|
|
68
|
+
}
|
|
69
|
+
if (isRecord(payload.reasoning)) {
|
|
70
|
+
extras.reasoning = structuredClone(payload.reasoning);
|
|
71
|
+
}
|
|
72
|
+
if (typeof payload.service_tier === "string" && payload.service_tier.trim().length > 0) {
|
|
73
|
+
extras.service_tier = payload.service_tier;
|
|
74
|
+
}
|
|
75
|
+
if (typeof payload.prompt_cache_key === "string" && payload.prompt_cache_key.trim().length > 0) {
|
|
76
|
+
extras.prompt_cache_key = payload.prompt_cache_key;
|
|
77
|
+
}
|
|
78
|
+
if (isRecord(payload.text)) {
|
|
79
|
+
extras.text = structuredClone(payload.text);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
cached = {
|
|
83
|
+
identity: structuredClone(identity),
|
|
84
|
+
extras,
|
|
85
|
+
};
|
|
86
|
+
} catch {
|
|
87
|
+
cached = undefined;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Return cached extras only for the exact provider/API/model/base URL/session identity. */
|
|
92
|
+
export function getCompactionRequestExtras(identity: RequestContextIdentity): CompactionRequestExtras | undefined {
|
|
93
|
+
if (!cached || !sameIdentity(cached.identity, identity)) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
return structuredClone(cached.extras);
|
|
99
|
+
} catch {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function clearRequestContextCache(): void {
|
|
105
|
+
cached = undefined;
|
|
106
|
+
}
|