pi-openai-toolkit 0.20.8 → 0.21.1
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 +8 -26
- package/README.zh.md +8 -26
- package/config.schema.json +5 -93
- package/docs/configuration.md +19 -31
- package/docs/internals.md +14 -23
- package/package.json +3 -13
- package/src/compact-client.ts +48 -25
- package/src/compaction-errors.ts +96 -0
- package/src/config/legacy.ts +519 -717
- package/src/config/migration.ts +11 -8
- package/src/config/notifications.ts +36 -28
- package/src/config/policy.ts +10 -18
- package/src/config/v2.ts +1167 -1380
- package/src/config.ts +9 -18
- package/src/context-management/manual-compact.ts +0 -11
- package/src/debug.ts +5 -4
- package/src/extension-runtime.ts +61 -15
- package/src/payload-rewrite.ts +2 -5
- package/src/remote-v2-client.ts +11 -14
- package/src/responses-websocket/transport.ts +136 -65
- package/src/runtime.ts +33 -0
- package/src/types.ts +14 -97
- package/tsconfig.check.json +0 -4
- package/src/auto-mode/classifier.ts +0 -161
- package/src/auto-mode/extension.ts +0 -691
- package/src/auto-mode/model-selection-guard.ts +0 -52
- package/src/auto-mode/policy.ts +0 -146
- package/src/auto-mode/prompt.ts +0 -279
- package/src/auto-mode/reviewer.ts +0 -325
- package/src/auto-mode/tool-review-tui.ts +0 -211
- package/src/auto-mode/transcript.ts +0 -135
- package/src/auto-mode/types.ts +0 -137
|
@@ -199,10 +199,69 @@ function buildCacheKey(model: Model<Api>, options: StreamOptions, requestHeaders
|
|
|
199
199
|
return [model.provider, model.id, cleanBaseUrl(model.baseUrl), options.sessionId, stableHash(JSON.stringify(identity))].join("\u0000");
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
// Only transport failures (not arbitrary caught errors) authorize auto fallback.
|
|
203
|
+
class RecoverableWebSocketError extends Error {}
|
|
204
|
+
|
|
205
|
+
const STREAM_INTERRUPTION = /\bstream closed before response\.completed\b/iu;
|
|
206
|
+
const TERMINAL_FAILURE_TEXT = /quota|billing|budget|usage.?limit|api.?key|unauthori[sz]ed|forbidden|authenticat|permission|invalid|malformed|unsupported|policy|context.?length|context.?overflow|\babort(?:ed)?\b|\bcancell?ed\b/iu;
|
|
207
|
+
const TRANSIENT_ERROR_CODES = new Set(["stream_error", "server_error", "request_timeout"]);
|
|
208
|
+
|
|
209
|
+
function transportError(error: unknown): Error {
|
|
210
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
211
|
+
if ((error instanceof Error && error.name === "AbortError") || TERMINAL_FAILURE_TEXT.test(message)) {
|
|
212
|
+
return error instanceof Error ? error : new Error(message);
|
|
213
|
+
}
|
|
214
|
+
return new RecoverableWebSocketError(/^WebSocket (?:error|closed|.*timeout)/iu.test(message)
|
|
215
|
+
? message : `WebSocket error: ${message}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
202
218
|
function socketError(event: WebSocketEvent): Error {
|
|
203
|
-
if (typeof event.message === "string" && event.message.length > 0) return
|
|
204
|
-
if (event.error instanceof Error) return event.error;
|
|
205
|
-
return
|
|
219
|
+
if (typeof event.message === "string" && event.message.length > 0) return transportError(event.message);
|
|
220
|
+
if (event.error instanceof Error) return transportError(event.error);
|
|
221
|
+
return transportError("WebSocket transport failed");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function socketCloseError(event: WebSocketEvent, streaming: boolean): Error {
|
|
225
|
+
const suffix = event.reason ? `: ${event.reason}` : "";
|
|
226
|
+
if ([1002, 1003, 1007, 1008].includes(event.code ?? 0) || TERMINAL_FAILURE_TEXT.test(event.reason ?? "")) {
|
|
227
|
+
return new Error(`Responses connection rejected${event.code ? ` (${event.code})` : ""}${suffix}`);
|
|
228
|
+
}
|
|
229
|
+
return transportError(streaming
|
|
230
|
+
? `WebSocket closed before response completion${suffix}`
|
|
231
|
+
: `WebSocket closed${event.code ? ` ${event.code}` : ""}${suffix}`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function compatibleFailureMetadata(error: Record<string, unknown>): boolean {
|
|
235
|
+
if (typeof error.message === "string" && TERMINAL_FAILURE_TEXT.test(error.message)) return false;
|
|
236
|
+
for (const field of ["code", "type"]) {
|
|
237
|
+
const value = error[field];
|
|
238
|
+
if (value !== undefined && value !== null && value !== "" &&
|
|
239
|
+
(typeof value !== "string" || !TRANSIENT_ERROR_CODES.has(value.toLowerCase()))) return false;
|
|
240
|
+
}
|
|
241
|
+
for (const field of ["status", "status_code"]) {
|
|
242
|
+
const value = error[field];
|
|
243
|
+
if (value !== undefined && value !== null &&
|
|
244
|
+
(typeof value !== "number" || ![408, 429, 500, 502, 503, 504].includes(value))) return false;
|
|
245
|
+
}
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function normalizeStreamInterruption(event: Record<string, unknown>): Record<string, unknown> | undefined {
|
|
250
|
+
const response = isRecord(event.response) ? event.response : undefined;
|
|
251
|
+
const error = event.type === "error" ? event : event.type === "response.failed" && isRecord(response?.error) ? response.error : undefined;
|
|
252
|
+
if (!error || typeof error.message !== "string" || !STREAM_INTERRUPTION.test(error.message) ||
|
|
253
|
+
TERMINAL_FAILURE_TEXT.test(error.message)) return undefined;
|
|
254
|
+
// Envelope type/status describe the event/response, not its failure category.
|
|
255
|
+
// Still check their code/HTTP status so a nested transient message cannot
|
|
256
|
+
// override explicit fatal metadata on an enclosing envelope.
|
|
257
|
+
const metadata: Record<string, unknown>[] = [{ ...event, type: undefined }];
|
|
258
|
+
if (event.type === "response.failed") {
|
|
259
|
+
metadata.push({ ...response, type: undefined, status: response?.status === "failed" ? undefined : response?.status }, error);
|
|
260
|
+
}
|
|
261
|
+
if (isRecord(error.error)) metadata.push(error.error);
|
|
262
|
+
if (metadata.some((value) => !compatibleFailureMetadata(value))) return undefined;
|
|
263
|
+
const normalized = { ...error, message: `WebSocket error: ${error.message}` };
|
|
264
|
+
return event.type === "error" ? normalized : { ...event, response: { ...response, error: normalized } };
|
|
206
265
|
}
|
|
207
266
|
|
|
208
267
|
function closeSocket(socket: WebSocketLike | undefined, reason = "done"): void {
|
|
@@ -247,8 +306,9 @@ export function closeResponsesWebSocketSessions(sessionId?: string): void {
|
|
|
247
306
|
}
|
|
248
307
|
|
|
249
308
|
|
|
250
|
-
function liveSignals(options: StreamOptions, init?: RequestInit): AbortSignal[] {
|
|
251
|
-
return [options.signal, init?.signal
|
|
309
|
+
function liveSignals(options: StreamOptions, input: RequestInfo | URL, init?: RequestInit): AbortSignal[] {
|
|
310
|
+
return [...new Set([options.signal, init?.signal, input instanceof Request ? input.signal : undefined]
|
|
311
|
+
.filter((signal): signal is AbortSignal => signal !== undefined && signal !== null))];
|
|
252
312
|
}
|
|
253
313
|
|
|
254
314
|
async function connectWebSocket(
|
|
@@ -258,7 +318,7 @@ async function connectWebSocket(
|
|
|
258
318
|
timeoutMs: number,
|
|
259
319
|
): Promise<WebSocketLike> {
|
|
260
320
|
const WebSocketCtor = globalThis.WebSocket as unknown as WebSocketConstructor | undefined;
|
|
261
|
-
if (typeof WebSocketCtor !== "function") throw
|
|
321
|
+
if (typeof WebSocketCtor !== "function") throw transportError("WebSocket transport is not available in this runtime");
|
|
262
322
|
if (signals.some((signal) => signal.aborted)) throw new Error("Request was aborted");
|
|
263
323
|
|
|
264
324
|
return new Promise((resolve, reject) => {
|
|
@@ -286,9 +346,7 @@ async function connectWebSocket(
|
|
|
286
346
|
resolve(socket!);
|
|
287
347
|
};
|
|
288
348
|
const onError = (event: WebSocketEvent) => fail(socketError(event));
|
|
289
|
-
const onClose = (event: WebSocketEvent) => fail(
|
|
290
|
-
`WebSocket closed${typeof event.code === "number" ? ` ${event.code}` : ""}${event.reason ? ` ${event.reason}` : ""}`,
|
|
291
|
-
));
|
|
349
|
+
const onClose = (event: WebSocketEvent) => fail(socketCloseError(event, false));
|
|
292
350
|
const onAbort = () => fail(new Error("Request was aborted"));
|
|
293
351
|
|
|
294
352
|
try {
|
|
@@ -301,7 +359,8 @@ async function connectWebSocket(
|
|
|
301
359
|
socket.addEventListener("error", onError);
|
|
302
360
|
socket.addEventListener("close", onClose);
|
|
303
361
|
for (const signal of signals) signal.addEventListener("abort", onAbort, { once: true });
|
|
304
|
-
if (
|
|
362
|
+
if (signals.some((signal) => signal.aborted)) { onAbort(); return; }
|
|
363
|
+
if (timeoutMs > 0) timeout = setTimeout(() => fail(transportError(`WebSocket connect timeout after ${timeoutMs}ms`)), timeoutMs);
|
|
305
364
|
});
|
|
306
365
|
}
|
|
307
366
|
|
|
@@ -439,22 +498,26 @@ function completedContinuation(entry: CachedSocketEntry | undefined, fullPayload
|
|
|
439
498
|
return true;
|
|
440
499
|
}
|
|
441
500
|
|
|
442
|
-
function createSseResponse(
|
|
501
|
+
async function createSseResponse(
|
|
443
502
|
socket: WebSocketLike,
|
|
444
503
|
signals: AbortSignal[],
|
|
445
504
|
idleTimeoutMs: number,
|
|
446
505
|
requestFrame: string,
|
|
447
506
|
onTerminal: (event: Record<string, unknown>) => boolean,
|
|
448
507
|
onRelease: (keep: boolean) => void,
|
|
449
|
-
|
|
508
|
+
allowBootstrapFallback: boolean,
|
|
509
|
+
): Promise<Response> {
|
|
450
510
|
const encoder = new TextEncoder();
|
|
451
|
-
let
|
|
511
|
+
let resolveReady!: () => void;
|
|
512
|
+
let rejectReady!: (error: unknown) => void;
|
|
513
|
+
const ready = new Promise<void>((resolve, reject) => { resolveReady = resolve; rejectReady = reject; });
|
|
514
|
+
let cancelBody: (reason?: unknown) => void = () => {};
|
|
452
515
|
const body = new ReadableStream<Uint8Array>({
|
|
453
516
|
start(controller) {
|
|
454
517
|
let closed = false;
|
|
455
|
-
let
|
|
518
|
+
let committed = false;
|
|
456
519
|
let idleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
457
|
-
let
|
|
520
|
+
let pending = Promise.resolve();
|
|
458
521
|
const cleanup = () => {
|
|
459
522
|
if (idleTimer) clearTimeout(idleTimer);
|
|
460
523
|
socket.removeEventListener("message", onMessage);
|
|
@@ -462,73 +525,75 @@ function createSseResponse(
|
|
|
462
525
|
socket.removeEventListener("close", onClose);
|
|
463
526
|
for (const signal of signals) signal.removeEventListener("abort", onAbort);
|
|
464
527
|
};
|
|
465
|
-
const release = (keep: boolean) => {
|
|
466
|
-
if (released) return;
|
|
467
|
-
released = true;
|
|
468
|
-
onRelease(keep);
|
|
469
|
-
};
|
|
470
|
-
const finish = () => {
|
|
471
|
-
if (closed) return;
|
|
472
|
-
closed = true;
|
|
473
|
-
cleanup();
|
|
474
|
-
controller.close();
|
|
475
|
-
};
|
|
476
528
|
const fail = (error: unknown) => {
|
|
477
529
|
if (closed) return;
|
|
478
530
|
closed = true;
|
|
479
531
|
cleanup();
|
|
480
|
-
|
|
481
|
-
|
|
532
|
+
onRelease(false);
|
|
533
|
+
if (!committed) {
|
|
534
|
+
if (signals.some((signal) => signal.aborted) || (allowBootstrapFallback && error instanceof RecoverableWebSocketError)) {
|
|
535
|
+
rejectReady(error);
|
|
536
|
+
} else {
|
|
537
|
+
// Return an errored body for deterministic failures/explicit WS.
|
|
538
|
+
// Rejecting fetch would make the SDK recast these as connection errors.
|
|
539
|
+
resolveReady();
|
|
540
|
+
}
|
|
541
|
+
}
|
|
482
542
|
controller.error(error);
|
|
483
543
|
};
|
|
484
544
|
const armIdleTimer = () => {
|
|
485
545
|
if (idleTimeoutMs <= 0) return;
|
|
486
546
|
if (idleTimer) clearTimeout(idleTimer);
|
|
487
|
-
idleTimer = setTimeout(() => fail(
|
|
488
|
-
};
|
|
489
|
-
const onMessage = (event: WebSocketEvent) => {
|
|
490
|
-
void (async () => {
|
|
491
|
-
try {
|
|
492
|
-
const text = await decodeSocketData(event.data);
|
|
493
|
-
if (!text) return;
|
|
494
|
-
const parsed: unknown = JSON.parse(text);
|
|
495
|
-
if (!isRecord(parsed) || typeof parsed.type !== "string") throw new Error("Invalid Responses WebSocket event");
|
|
496
|
-
controller.enqueue(encoder.encode(`event: ${parsed.type}\ndata: ${JSON.stringify(parsed)}\n\n`));
|
|
497
|
-
armIdleTimer();
|
|
498
|
-
if (TERMINAL_EVENTS.has(parsed.type)) {
|
|
499
|
-
sawTerminal = true;
|
|
500
|
-
const keep = onTerminal(parsed);
|
|
501
|
-
release(keep);
|
|
502
|
-
finish();
|
|
503
|
-
}
|
|
504
|
-
} catch (error) {
|
|
505
|
-
fail(error);
|
|
506
|
-
}
|
|
507
|
-
})();
|
|
547
|
+
idleTimer = setTimeout(() => fail(transportError(`WebSocket idle timeout after ${idleTimeoutMs}ms`)), idleTimeoutMs);
|
|
508
548
|
};
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
if (sawTerminal) finish();
|
|
512
|
-
else fail(new Error(`WebSocket closed before response completion${event.reason ? `: ${event.reason}` : ""}`));
|
|
549
|
+
const enqueue = (operation: () => Promise<void> | void) => {
|
|
550
|
+
pending = pending.then(() => { if (!closed) return operation(); }).catch(fail);
|
|
513
551
|
};
|
|
552
|
+
const onMessage = (event: WebSocketEvent) => enqueue(async () => {
|
|
553
|
+
const text = await decodeSocketData(event.data);
|
|
554
|
+
if (closed) return;
|
|
555
|
+
if (text === undefined) throw new Error("Invalid Responses WebSocket frame");
|
|
556
|
+
const parsed: unknown = JSON.parse(text);
|
|
557
|
+
if (!isRecord(parsed) || typeof parsed.type !== "string" || !parsed.type) throw new Error("Invalid Responses WebSocket event");
|
|
558
|
+
const normalized = normalizeStreamInterruption(parsed);
|
|
559
|
+
if (!committed && allowBootstrapFallback && normalized) {
|
|
560
|
+
fail(transportError("stream closed before response.completed"));
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const forwarded = normalized ?? parsed;
|
|
564
|
+
controller.enqueue(encoder.encode(`event: ${forwarded.type}\ndata: ${JSON.stringify(forwarded)}\n\n`));
|
|
565
|
+
committed = true;
|
|
566
|
+
resolveReady();
|
|
567
|
+
armIdleTimer();
|
|
568
|
+
if (TERMINAL_EVENTS.has(parsed.type)) {
|
|
569
|
+
closed = true;
|
|
570
|
+
cleanup();
|
|
571
|
+
const keep = onTerminal(parsed);
|
|
572
|
+
onRelease(keep);
|
|
573
|
+
controller.close();
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
const onError = (event: WebSocketEvent) => enqueue(() => fail(socketError(event)));
|
|
577
|
+
const onClose = (event: WebSocketEvent) => enqueue(() => fail(socketCloseError(event, true)));
|
|
578
|
+
// Abort/timeout deliberately bypass decoding: a stuck decoder must not
|
|
579
|
+
// prevent cancellation. Its late result sees closed and is discarded.
|
|
514
580
|
const onAbort = () => fail(new Error("Request was aborted"));
|
|
515
|
-
|
|
516
581
|
cancelBody = (reason) => fail(reason instanceof Error ? reason : new Error("Response stream was cancelled"));
|
|
517
582
|
socket.addEventListener("message", onMessage);
|
|
518
583
|
socket.addEventListener("error", onError);
|
|
519
584
|
socket.addEventListener("close", onClose);
|
|
520
585
|
for (const signal of signals) signal.addEventListener("abort", onAbort, { once: true });
|
|
586
|
+
if (signals.some((signal) => signal.aborted)) { onAbort(); return; }
|
|
521
587
|
armIdleTimer();
|
|
522
588
|
try {
|
|
523
589
|
socket.send(requestFrame);
|
|
524
590
|
} catch (error) {
|
|
525
|
-
fail(error);
|
|
591
|
+
fail(transportError(error));
|
|
526
592
|
}
|
|
527
593
|
},
|
|
528
|
-
cancel(reason) {
|
|
529
|
-
cancelBody(reason);
|
|
530
|
-
},
|
|
594
|
+
cancel(reason) { cancelBody(reason); },
|
|
531
595
|
});
|
|
596
|
+
await ready;
|
|
532
597
|
return new Response(body, {
|
|
533
598
|
status: 200,
|
|
534
599
|
headers: { "content-type": "text/event-stream", connection: "keep-alive" },
|
|
@@ -573,12 +638,12 @@ export function createResponsesWebSocketFetch(model: Model<Api>, options: Stream
|
|
|
573
638
|
}
|
|
574
639
|
if (!isRecord(payload)) return fallback(input, init);
|
|
575
640
|
|
|
576
|
-
const signals = liveSignals(options, init);
|
|
641
|
+
const signals = liveSignals(options, input, init);
|
|
577
642
|
const requestId = options.sessionId?.slice(0, 64) || crypto.randomUUID();
|
|
578
643
|
const fetchHeaders = requestHeaders(input, init);
|
|
579
644
|
const headers = buildWebSocketHeaders(model, options, fetchHeaders, requestId);
|
|
580
645
|
const cacheKey = shouldCacheSocket(options) ? buildCacheKey(model, options, fetchHeaders) : undefined;
|
|
581
|
-
let
|
|
646
|
+
let release: ((keep: boolean) => void) | undefined;
|
|
582
647
|
try {
|
|
583
648
|
const lease = await acquireWebSocket(
|
|
584
649
|
buildResponsesWebSocketUrl(model),
|
|
@@ -588,21 +653,27 @@ export function createResponsesWebSocketFetch(model: Model<Api>, options: Stream
|
|
|
588
653
|
cacheKey,
|
|
589
654
|
options.sessionId,
|
|
590
655
|
);
|
|
591
|
-
|
|
656
|
+
let released = false;
|
|
657
|
+
release = (keep) => {
|
|
658
|
+
if (released) return;
|
|
659
|
+
released = true;
|
|
660
|
+
lease.release(keep);
|
|
661
|
+
};
|
|
592
662
|
const fullPayload = payload;
|
|
593
663
|
const requestPayload = lease.entry ? buildCachedRequestBody(lease.entry, fullPayload) : fullPayload;
|
|
594
664
|
const requestFrame = JSON.stringify({ type: "response.create", ...requestPayload });
|
|
595
|
-
return createSseResponse(
|
|
665
|
+
return await createSseResponse(
|
|
596
666
|
lease.socket,
|
|
597
667
|
signals,
|
|
598
668
|
options.timeoutMs ?? 0,
|
|
599
669
|
requestFrame,
|
|
600
670
|
(event) => completedContinuation(lease.entry, fullPayload, event),
|
|
601
|
-
|
|
671
|
+
release,
|
|
672
|
+
transportMode(options) === "auto",
|
|
602
673
|
);
|
|
603
674
|
} catch (error) {
|
|
604
|
-
|
|
605
|
-
if (transportMode(options) === "auto" && !signals.some((signal) => signal.aborted)) {
|
|
675
|
+
release?.(false);
|
|
676
|
+
if (error instanceof RecoverableWebSocketError && transportMode(options) === "auto" && !signals.some((signal) => signal.aborted)) {
|
|
606
677
|
return fallback(input, init);
|
|
607
678
|
}
|
|
608
679
|
throw error;
|
package/src/runtime.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { convertToLlm, type ExtensionAPI, type ExtensionContext } from "@earendi
|
|
|
4
4
|
import { isExactModelAllowed } from "./model-scope";
|
|
5
5
|
import {
|
|
6
6
|
CODEX_AFFINITY_SCOPE,
|
|
7
|
+
buildResponsesRequestHeaders,
|
|
7
8
|
type CodexAffinity,
|
|
8
9
|
} from "./responses-headers";
|
|
9
10
|
import { RESPONSES_COMPACT_CAPABLE_APIS } from "./types";
|
|
@@ -86,6 +87,8 @@ export type ResponsesRuntime = {
|
|
|
86
87
|
};
|
|
87
88
|
|
|
88
89
|
export type NativeCompactionRuntime = ResponsesRuntime & {
|
|
90
|
+
/** Public host snapshot for the resolved physical model; never inferred from token text. */
|
|
91
|
+
authMode?: "oauth" | "non-oauth" | "unknown";
|
|
89
92
|
compactPath: string;
|
|
90
93
|
compactUrl: string;
|
|
91
94
|
payload?: ResponsesCompatibleRequestPayload;
|
|
@@ -503,6 +506,35 @@ async function resolveRequestAuth(ctx: ExtensionContext, model: RuntimeModel): P
|
|
|
503
506
|
return modelRegistry.getApiKeyAndHeaders(model);
|
|
504
507
|
}
|
|
505
508
|
|
|
509
|
+
function resolveAuthMode(ctx: ExtensionContext, model: RuntimeModel): NativeCompactionRuntime["authMode"] {
|
|
510
|
+
const registry = ctx.modelRegistry as unknown as {
|
|
511
|
+
isUsingOAuth?: (model: RuntimeModel) => unknown;
|
|
512
|
+
hasConfiguredAuth?: (model: RuntimeModel) => unknown;
|
|
513
|
+
};
|
|
514
|
+
try {
|
|
515
|
+
if (typeof registry.isUsingOAuth !== "function" || typeof registry.hasConfiguredAuth !== "function" ||
|
|
516
|
+
registry.hasConfiguredAuth(model) !== true) return "unknown";
|
|
517
|
+
const oauth = registry.isUsingOAuth(model);
|
|
518
|
+
return oauth === true ? "oauth" : oauth === false ? "non-oauth" : "unknown";
|
|
519
|
+
} catch {
|
|
520
|
+
return "unknown";
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** Conservative automatic standalone routing; provider aliases alone prove nothing. */
|
|
525
|
+
export function isStandaloneCompactionEligible(runtime: NativeCompactionRuntime): boolean {
|
|
526
|
+
if (runtime.api !== "openai-responses" || runtime.codexAffinity || runtime.authMode !== "non-oauth") return false;
|
|
527
|
+
try {
|
|
528
|
+
const url = new URL(runtime.compactUrl);
|
|
529
|
+
if (url.protocol !== "https:" || url.hostname !== "api.openai.com" || url.port ||
|
|
530
|
+
url.pathname !== "/v1/responses/compact" || url.username || url.password || url.search || url.hash) return false;
|
|
531
|
+
const headers = buildResponsesRequestHeaders(runtime, { accept: "application/json" });
|
|
532
|
+
return headers.get("authorization") === `Bearer ${runtime.apiKey}`;
|
|
533
|
+
} catch {
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
506
538
|
export function isSupportedApi(api: string): api is ResponsesCompactApi {
|
|
507
539
|
return (RESPONSES_COMPACT_CAPABLE_APIS as readonly string[]).includes(api);
|
|
508
540
|
}
|
|
@@ -696,6 +728,7 @@ async function resolveNativeCompactionEnvironmentForModel(
|
|
|
696
728
|
requestModel,
|
|
697
729
|
baseUrl,
|
|
698
730
|
apiKey: auth.apiKey,
|
|
731
|
+
authMode: resolveAuthMode(ctx, effectiveModel),
|
|
699
732
|
env: auth.env,
|
|
700
733
|
headers: auth.headers,
|
|
701
734
|
responsesPath: buildResponsesPath(descriptor.api),
|
package/src/types.ts
CHANGED
|
@@ -6,7 +6,6 @@ export const TOOLKIT_ID = "pi-openai-toolkit";
|
|
|
6
6
|
export const COMPACTION_EXTENSION_ID = `${TOOLKIT_ID}:compaction`;
|
|
7
7
|
export const WEB_SEARCH_EXTENSION_ID = `${TOOLKIT_ID}:web-search`;
|
|
8
8
|
export const IMAGE_GENERATION_EXTENSION_ID = `${TOOLKIT_ID}:image-generation`;
|
|
9
|
-
export const AUTO_MODE_EXTENSION_ID = `${TOOLKIT_ID}:auto-mode`;
|
|
10
9
|
export const DEFAULT_ARTIFACT_ROOT = "~/.pi/agent/artifacts/pi-openai-toolkit/compaction";
|
|
11
10
|
export const REDACTED_VALUE = "[REDACTED]";
|
|
12
11
|
/**
|
|
@@ -43,6 +42,7 @@ export type NativeCompactionInputProvenance =
|
|
|
43
42
|
* replay shim; the provider receives the opaque item from details.compactedWindow.
|
|
44
43
|
*/
|
|
45
44
|
export const NATIVE_COMPACTION_FALLBACK_SUMMARY = "[OpenAI remote v2 opaque compaction checkpoint]";
|
|
45
|
+
export const STANDALONE_COMPACTION_FALLBACK_SUMMARY = "[OpenAI standalone opaque compaction window]";
|
|
46
46
|
|
|
47
47
|
export const THINKING_LEVELS: readonly ThinkingLevel[] = [
|
|
48
48
|
"off",
|
|
@@ -165,71 +165,10 @@ export type ImageGenerationConfig = {
|
|
|
165
165
|
models: string[];
|
|
166
166
|
};
|
|
167
167
|
|
|
168
|
-
/** `side-effect` reviews bash/write/edit plus extras; `all` reviews every tool call. */
|
|
169
|
-
export type AutoModeGate = "side-effect" | "all";
|
|
170
|
-
|
|
171
|
-
export type AutoModeConfig = {
|
|
172
|
-
enabled: boolean;
|
|
173
|
-
/** Exact provider/model keys whose sessions may engage auto mode. */
|
|
174
|
-
models: string[];
|
|
175
|
-
/** "provider/model-id" that approves gated tool calls while auto mode is engaged. */
|
|
176
|
-
reviewerModel?: string;
|
|
177
|
-
gate: AutoModeGate;
|
|
178
|
-
/** Extra tool names reviewed on top of the side-effect default set. */
|
|
179
|
-
extraTools: string[];
|
|
180
|
-
/** Reviewer deadline in ms; expiry degrades to human confirmation or fail-closed. */
|
|
181
|
-
timeoutMs: number;
|
|
182
|
-
/** Give the reviewer a bounded transcript so it can judge user authorization. */
|
|
183
|
-
transcript: boolean;
|
|
184
|
-
/** Let the reviewer spend read-only tool calls establishing facts before deciding. */
|
|
185
|
-
evidenceTools: boolean;
|
|
186
|
-
maxEvidenceRounds: number;
|
|
187
|
-
/** Non-blocking trajectory pre-scorer that can satisfy ordinary gated calls. */
|
|
188
|
-
classifier: AutoModeClassifierConfig;
|
|
189
|
-
/** End a turn that keeps getting denied instead of negotiating forever. */
|
|
190
|
-
circuitBreaker: AutoModeCircuitBreakerConfig;
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
export type AutoModeClassifierConfig = {
|
|
194
|
-
enabled: boolean;
|
|
195
|
-
/** "provider/model-id"; falls back to `autoMode.reviewerModel` when unset. */
|
|
196
|
-
model?: string;
|
|
197
|
-
timeoutMs: number;
|
|
198
|
-
/** How many tool calls may elapse between scoring and use before a score is stale. */
|
|
199
|
-
maxLag: number;
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
export type AutoModeCircuitBreakerConfig = {
|
|
203
|
-
/** Consecutive reviewer denials in one turn that end the turn. 0 disables. */
|
|
204
|
-
consecutiveDenials: number;
|
|
205
|
-
/** Denials inside `windowSize` reviewed calls that end the turn. 0 disables. */
|
|
206
|
-
recentDenials: number;
|
|
207
|
-
windowSize: number;
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
export const REVIEWER_TIMEOUT_MIN_MS = 1_000;
|
|
211
|
-
export const REVIEWER_TIMEOUT_MAX_MS = 120_000;
|
|
212
|
-
|
|
213
|
-
/** Read-only investigation rounds the reviewer may spend before it must answer. */
|
|
214
|
-
export const EVIDENCE_ROUNDS_MIN = 0;
|
|
215
|
-
export const EVIDENCE_ROUNDS_MAX = 8;
|
|
216
|
-
|
|
217
|
-
export const CLASSIFIER_MAX_LAG_MIN = 0;
|
|
218
|
-
export const CLASSIFIER_MAX_LAG_MAX = 20;
|
|
219
|
-
|
|
220
|
-
export const DEFAULT_CLASSIFIER_TIMEOUT_MS = 15_000;
|
|
221
|
-
|
|
222
|
-
/** Denial counts are small integers; 0 disables the matching trigger. */
|
|
223
|
-
export const BREAKER_LIMIT_MIN = 0;
|
|
224
|
-
export const BREAKER_LIMIT_MAX = 100;
|
|
225
|
-
export const BREAKER_WINDOW_MIN = 1;
|
|
226
|
-
export const BREAKER_WINDOW_MAX = 200;
|
|
227
|
-
|
|
228
168
|
export type ToolkitConfig = {
|
|
229
169
|
compaction: CompactionConfig;
|
|
230
170
|
webSearch: WebSearchConfig;
|
|
231
171
|
imageGeneration: ImageGenerationConfig;
|
|
232
|
-
autoMode: AutoModeConfig;
|
|
233
172
|
};
|
|
234
173
|
|
|
235
174
|
export type LoadedToolkitConfig = {
|
|
@@ -324,6 +263,7 @@ export type NativeCompactionDetails = NativeCompactionIdentity & {
|
|
|
324
263
|
export type NativeCompactionEntry = CompactionEntry<NativeCompactionDetails>;
|
|
325
264
|
|
|
326
265
|
export type CreateNativeCompactionDetailsInput = NativeCompactionIdentity & {
|
|
266
|
+
strategy?: NativeCompactionStrategy;
|
|
327
267
|
inputProvenance: NativeCompactionInputProvenance;
|
|
328
268
|
compactionModel?: NativeCompactionIdentity;
|
|
329
269
|
deferredToolCarryover?: DeferredToolCarryoverV1;
|
|
@@ -338,8 +278,8 @@ export type CreateNativeCompactionResultInput = {
|
|
|
338
278
|
tokensBefore: number;
|
|
339
279
|
details: NativeCompactionDetails;
|
|
340
280
|
/**
|
|
341
|
-
*
|
|
342
|
-
*
|
|
281
|
+
* An explicitly supplied genuine summary. Retained standalone assistant text is
|
|
282
|
+
* not a summary of the sealed history; ordinary remote success uses an opaque marker.
|
|
343
283
|
*/
|
|
344
284
|
summary?: string;
|
|
345
285
|
};
|
|
@@ -364,7 +304,7 @@ function isStructuredValue(value: unknown): boolean {
|
|
|
364
304
|
if (
|
|
365
305
|
value === null ||
|
|
366
306
|
typeof value === "string" ||
|
|
367
|
-
typeof value === "number" ||
|
|
307
|
+
(typeof value === "number" && Number.isFinite(value)) ||
|
|
368
308
|
typeof value === "boolean"
|
|
369
309
|
) {
|
|
370
310
|
return true;
|
|
@@ -396,17 +336,13 @@ function cloneStructuredValue(value: unknown): unknown {
|
|
|
396
336
|
}
|
|
397
337
|
|
|
398
338
|
if (isRecord(value)) {
|
|
399
|
-
|
|
400
|
-
for (const [key, nested] of Object.entries(value)) {
|
|
401
|
-
clone[key] = cloneStructuredValue(nested);
|
|
402
|
-
}
|
|
403
|
-
return clone;
|
|
339
|
+
return Object.fromEntries(Object.entries(value).map(([key, nested]) => [key, cloneStructuredValue(nested)]));
|
|
404
340
|
}
|
|
405
341
|
|
|
406
342
|
throw new Error(`Unsupported structured value: ${typeof value}`);
|
|
407
343
|
}
|
|
408
344
|
|
|
409
|
-
function isCompactedWindowItem(value: unknown): value is Record<string, unknown> {
|
|
345
|
+
export function isCompactedWindowItem(value: unknown): value is Record<string, unknown> {
|
|
410
346
|
return isRecord(value) && Object.values(value).every(isStructuredValue);
|
|
411
347
|
}
|
|
412
348
|
|
|
@@ -533,8 +469,11 @@ export function isNativeCompactionEntry(value: unknown): value is NativeCompacti
|
|
|
533
469
|
}
|
|
534
470
|
|
|
535
471
|
export function createNativeCompactionDetails(input: CreateNativeCompactionDetailsInput): NativeCompactionDetails {
|
|
472
|
+
if (input.strategy !== undefined && input.strategy !== LEGACY_NATIVE_COMPACTION_STRATEGY && input.strategy !== REMOTE_V2_COMPACTION_STRATEGY) {
|
|
473
|
+
throw new Error("Invalid native compaction strategy");
|
|
474
|
+
}
|
|
536
475
|
return {
|
|
537
|
-
strategy: NATIVE_COMPACTION_STRATEGY,
|
|
476
|
+
strategy: input.strategy ?? NATIVE_COMPACTION_STRATEGY,
|
|
538
477
|
inputProvenance: input.inputProvenance,
|
|
539
478
|
provider: normalizeString(input.provider),
|
|
540
479
|
api: normalizeString(input.api),
|
|
@@ -570,7 +509,9 @@ export function createNativeCompactionResult(
|
|
|
570
509
|
): CompactionResult<NativeCompactionDetails> {
|
|
571
510
|
const summary = input.summary?.trim();
|
|
572
511
|
return {
|
|
573
|
-
summary: summary && summary.length > 0 ? summary
|
|
512
|
+
summary: summary && summary.length > 0 ? summary
|
|
513
|
+
: input.details.strategy === LEGACY_NATIVE_COMPACTION_STRATEGY ? STANDALONE_COMPACTION_FALLBACK_SUMMARY
|
|
514
|
+
: NATIVE_COMPACTION_FALLBACK_SUMMARY,
|
|
574
515
|
firstKeptEntryId: input.firstKeptEntryId,
|
|
575
516
|
tokensBefore: input.tokensBefore,
|
|
576
517
|
details: input.details,
|
|
@@ -617,32 +558,8 @@ export const DEFAULT_IMAGE_GENERATION_CONFIG: ImageGenerationConfig = {
|
|
|
617
558
|
models: [...DEFAULT_IMAGE_GENERATION_MODELS],
|
|
618
559
|
};
|
|
619
560
|
|
|
620
|
-
export const DEFAULT_AUTO_MODE_CONFIG: AutoModeConfig = {
|
|
621
|
-
enabled: true,
|
|
622
|
-
models: [],
|
|
623
|
-
reviewerModel: undefined,
|
|
624
|
-
gate: "side-effect",
|
|
625
|
-
extraTools: [],
|
|
626
|
-
timeoutMs: 30_000,
|
|
627
|
-
transcript: true,
|
|
628
|
-
evidenceTools: true,
|
|
629
|
-
maxEvidenceRounds: 3,
|
|
630
|
-
classifier: {
|
|
631
|
-
enabled: false,
|
|
632
|
-
model: undefined,
|
|
633
|
-
timeoutMs: DEFAULT_CLASSIFIER_TIMEOUT_MS,
|
|
634
|
-
maxLag: 2,
|
|
635
|
-
},
|
|
636
|
-
circuitBreaker: {
|
|
637
|
-
consecutiveDenials: 3,
|
|
638
|
-
recentDenials: 10,
|
|
639
|
-
windowSize: 50,
|
|
640
|
-
},
|
|
641
|
-
};
|
|
642
|
-
|
|
643
561
|
export const DEFAULT_TOOLKIT_CONFIG: ToolkitConfig = {
|
|
644
562
|
compaction: DEFAULT_COMPACTION_CONFIG,
|
|
645
563
|
webSearch: DEFAULT_WEB_SEARCH_CONFIG,
|
|
646
564
|
imageGeneration: DEFAULT_IMAGE_GENERATION_CONFIG,
|
|
647
|
-
autoMode: DEFAULT_AUTO_MODE_CONFIG,
|
|
648
565
|
};
|
package/tsconfig.check.json
CHANGED
|
@@ -29,10 +29,6 @@
|
|
|
29
29
|
"src/image-generation/render.ts",
|
|
30
30
|
"src/image-generation/service.ts",
|
|
31
31
|
"src/image-generation/types.ts",
|
|
32
|
-
"src/auto-mode/extension.ts",
|
|
33
|
-
"src/auto-mode/policy.ts",
|
|
34
|
-
"src/auto-mode/reviewer.ts",
|
|
35
|
-
"src/auto-mode/types.ts",
|
|
36
32
|
"src/responses-websocket/extension.ts",
|
|
37
33
|
"src/responses-websocket/transport.ts",
|
|
38
34
|
"src/model-scope.ts",
|