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
package/src/compact-client.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { writeDebugArtifact } from "./debug";
|
|
2
|
+
import { decodeCompactionError, readCompactionResponseText, type CompactionErrorInfo } from "./compaction-errors";
|
|
2
3
|
import { buildResponsesRequestHeaders } from "./responses-headers";
|
|
3
4
|
import { stripConfigurationUpdateItems } from "./remote-v2-client";
|
|
4
5
|
import type { NativeCompactionRuntime } from "./runtime";
|
|
5
6
|
import type { NativeCompactionRequestBody } from "./serializer";
|
|
6
|
-
import type
|
|
7
|
+
import { isCompactedWindowItem, type ArtifactContext, type CompactionConfig } from "./types";
|
|
7
8
|
|
|
8
9
|
const JSON_CONTENT_TYPE = "application/json";
|
|
9
10
|
|
|
@@ -21,7 +22,9 @@ export type NativeCompactionClientFailureReason =
|
|
|
21
22
|
| "empty-body"
|
|
22
23
|
| "invalid-json"
|
|
23
24
|
| "malformed-response"
|
|
24
|
-
| "empty-output"
|
|
25
|
+
| "empty-output"
|
|
26
|
+
| "missing-compaction"
|
|
27
|
+
| "malformed-compaction-item";
|
|
25
28
|
|
|
26
29
|
export type NativeCompactionClientSuccess = {
|
|
27
30
|
ok: true;
|
|
@@ -29,12 +32,12 @@ export type NativeCompactionClientSuccess = {
|
|
|
29
32
|
compactedWindow: unknown[];
|
|
30
33
|
compactResponseId?: string;
|
|
31
34
|
createdAt?: string;
|
|
32
|
-
/**
|
|
35
|
+
/** Retained assistant text only; not a portable summary of the opaque window. */
|
|
33
36
|
summaryText?: string;
|
|
34
37
|
response: CompactResponseEnvelope;
|
|
35
38
|
};
|
|
36
39
|
|
|
37
|
-
export type NativeCompactionClientFailure = {
|
|
40
|
+
export type NativeCompactionClientFailure = CompactionErrorInfo & {
|
|
38
41
|
ok: false;
|
|
39
42
|
reason: NativeCompactionClientFailureReason;
|
|
40
43
|
status?: number;
|
|
@@ -83,19 +86,11 @@ function normalizeResponseTimestamp(value: unknown): string | undefined {
|
|
|
83
86
|
return Number.isNaN(parsed) ? trimmed : new Date(parsed).toISOString();
|
|
84
87
|
}
|
|
85
88
|
|
|
86
|
-
function isCompactOutputItem(value: unknown): value is Record<string, unknown> {
|
|
87
|
-
return isRecord(value);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
89
|
function isCompactResponseEnvelope(value: unknown): value is CompactResponseEnvelope {
|
|
91
|
-
return isRecord(value) && Array.isArray(value.output) && value.output.every(
|
|
90
|
+
return isRecord(value) && Array.isArray(value.output) && value.output.every(isCompactedWindowItem);
|
|
92
91
|
}
|
|
93
92
|
|
|
94
|
-
/**
|
|
95
|
-
* Extract the assistant-authored summary text from the compacted window so the
|
|
96
|
-
* persisted CompactionEntry.summary carries real context. Without this, switching
|
|
97
|
-
* to a non-Responses model later would replay a meaningless placeholder.
|
|
98
|
-
*/
|
|
93
|
+
/** Extract retained assistant text. It must not be used as an opaque-window summary. */
|
|
99
94
|
export function extractCompactedSummaryText(output: readonly unknown[]): string | undefined {
|
|
100
95
|
const texts: string[] = [];
|
|
101
96
|
for (const item of output) {
|
|
@@ -131,7 +126,24 @@ function writeCompactArtifact(
|
|
|
131
126
|
return;
|
|
132
127
|
}
|
|
133
128
|
|
|
134
|
-
writeDebugArtifact("compact-response", data, settings, context);
|
|
129
|
+
writeDebugArtifact("compact-response", isRecord(data) ? { ...data, protocol: "standalone-compact" } : { protocol: "standalone-compact", data }, settings, context);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Installed SDK CompactParams allowlist; complete input makes previous_response_id unnecessary. */
|
|
133
|
+
export function buildStandaloneCompactionRequest(request: NativeCompactionRequestBody): NativeCompactionRequestBody {
|
|
134
|
+
const body: NativeCompactionRequestBody = {
|
|
135
|
+
model: request.model,
|
|
136
|
+
input: structuredClone(stripConfigurationUpdateItems(request.input)) as NativeCompactionRequestBody["input"],
|
|
137
|
+
instructions: request.instructions,
|
|
138
|
+
};
|
|
139
|
+
if (["auto", "default", "fast", "flex", "priority"].includes(request.service_tier ?? "")) body.service_tier = request.service_tier;
|
|
140
|
+
if (typeof request.prompt_cache_key === "string") body.prompt_cache_key = request.prompt_cache_key;
|
|
141
|
+
if (request.prompt_cache_retention === "in_memory" || request.prompt_cache_retention === "24h") body.prompt_cache_retention = request.prompt_cache_retention;
|
|
142
|
+
const cache = request.prompt_cache_options;
|
|
143
|
+
if (isRecord(cache) && Object.keys(cache).every((key) => key === "mode" || key === "ttl") &&
|
|
144
|
+
(cache.mode === undefined || cache.mode === "implicit" || cache.mode === "explicit") &&
|
|
145
|
+
(cache.ttl === undefined || cache.ttl === "30m")) body.prompt_cache_options = structuredClone(cache);
|
|
146
|
+
return body;
|
|
135
147
|
}
|
|
136
148
|
|
|
137
149
|
export async function executeNativeCompaction(
|
|
@@ -140,10 +152,7 @@ export async function executeNativeCompaction(
|
|
|
140
152
|
const { runtime, signal, settings, context } = options;
|
|
141
153
|
// Same Astra-history rejection as remote v2: `/responses/compact` rejects
|
|
142
154
|
// `configuration_update` items, so a replayed history never carries one.
|
|
143
|
-
const request
|
|
144
|
-
...options.request,
|
|
145
|
-
input: stripConfigurationUpdateItems(options.request.input) as NativeCompactionRequestBody["input"],
|
|
146
|
-
};
|
|
155
|
+
const request = buildStandaloneCompactionRequest(options.request);
|
|
147
156
|
const headers = toHeaders(runtime);
|
|
148
157
|
|
|
149
158
|
if (signal?.aborted) {
|
|
@@ -171,9 +180,10 @@ export async function executeNativeCompaction(
|
|
|
171
180
|
method: "POST",
|
|
172
181
|
headers,
|
|
173
182
|
body: JSON.stringify(request),
|
|
183
|
+
redirect: "error",
|
|
174
184
|
signal,
|
|
175
185
|
});
|
|
176
|
-
const responseText = await response
|
|
186
|
+
const responseText = await readCompactionResponseText(response, signal);
|
|
177
187
|
const responseHeaders: Record<string, string> = {};
|
|
178
188
|
response.headers.forEach((value, key) => {
|
|
179
189
|
responseHeaders[key] = value;
|
|
@@ -193,6 +203,7 @@ export async function executeNativeCompaction(
|
|
|
193
203
|
ok: false,
|
|
194
204
|
reason: "non-2xx",
|
|
195
205
|
status: response.status,
|
|
206
|
+
...decodeCompactionError(responseJson),
|
|
196
207
|
responseText: responseText || undefined,
|
|
197
208
|
responseJson,
|
|
198
209
|
};
|
|
@@ -245,12 +256,12 @@ export async function executeNativeCompaction(
|
|
|
245
256
|
let parsed: unknown;
|
|
246
257
|
try {
|
|
247
258
|
parsed = JSON.parse(responseText);
|
|
248
|
-
} catch
|
|
259
|
+
} catch {
|
|
249
260
|
const failure: NativeCompactionClientFailure = {
|
|
250
261
|
ok: false,
|
|
251
262
|
reason: "invalid-json",
|
|
252
263
|
status: response.status,
|
|
253
|
-
errorMessage:
|
|
264
|
+
errorMessage: "The compact endpoint returned invalid JSON.",
|
|
254
265
|
responseText,
|
|
255
266
|
};
|
|
256
267
|
writeCompactArtifact(
|
|
@@ -327,10 +338,22 @@ export async function executeNativeCompaction(
|
|
|
327
338
|
return failure;
|
|
328
339
|
}
|
|
329
340
|
|
|
341
|
+
const checkpoints = parsed.output.filter((item) => isRecord(item) && item.type === "compaction");
|
|
342
|
+
const malformed = checkpoints.some((item) => !isRecord(item) || typeof item.encrypted_content !== "string" || !item.encrypted_content.trim());
|
|
343
|
+
if (!checkpoints.length || malformed) {
|
|
344
|
+
const failure: NativeCompactionClientFailure = {
|
|
345
|
+
ok: false,
|
|
346
|
+
reason: malformed ? "malformed-compaction-item" : "missing-compaction",
|
|
347
|
+
status: response.status,
|
|
348
|
+
};
|
|
349
|
+
writeCompactArtifact({ protocol: "standalone-compact", outcome: failure }, settings, context);
|
|
350
|
+
return failure;
|
|
351
|
+
}
|
|
352
|
+
if (signal?.aborted) return { ok: false, reason: "aborted" };
|
|
330
353
|
const success: NativeCompactionClientSuccess = {
|
|
331
354
|
ok: true,
|
|
332
355
|
status: response.status,
|
|
333
|
-
compactedWindow:
|
|
356
|
+
compactedWindow: structuredClone(parsed.output),
|
|
334
357
|
compactResponseId: typeof parsed.id === "string" && parsed.id.trim() ? parsed.id.trim() : undefined,
|
|
335
358
|
createdAt: normalizeResponseTimestamp(parsed.created_at),
|
|
336
359
|
summaryText: extractCompactedSummaryText(parsed.output),
|
|
@@ -361,7 +384,7 @@ export async function executeNativeCompaction(
|
|
|
361
384
|
);
|
|
362
385
|
return success;
|
|
363
386
|
} catch (error) {
|
|
364
|
-
const failure: NativeCompactionClientFailure = isAbortError(error)
|
|
387
|
+
const failure: NativeCompactionClientFailure = signal?.aborted || isAbortError(error)
|
|
365
388
|
? {
|
|
366
389
|
ok: false,
|
|
367
390
|
reason: "aborted",
|
|
@@ -369,7 +392,7 @@ export async function executeNativeCompaction(
|
|
|
369
392
|
: {
|
|
370
393
|
ok: false,
|
|
371
394
|
reason: "network-error",
|
|
372
|
-
errorMessage:
|
|
395
|
+
errorMessage: "Compaction transport failed; no remote checkpoint was produced.",
|
|
373
396
|
};
|
|
374
397
|
|
|
375
398
|
writeCompactArtifact(
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** Content-free diagnostics shared by the compaction HTTP/stream boundaries. */
|
|
2
|
+
export type CompactionErrorInfo = {
|
|
3
|
+
errorCode?: string;
|
|
4
|
+
errorType?: string;
|
|
5
|
+
errorParam?: string;
|
|
6
|
+
/** Controlled text, never a provider-authored message. */
|
|
7
|
+
errorMessage?: string;
|
|
8
|
+
errorCategory?: "auth" | "quota" | "protocol";
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const HINTS: Record<string, string> = {
|
|
12
|
+
subscription_sharing_unsupported_capability: "This OpenAI subscription-sharing grant does not authorize compaction. Use a separately authenticated supported API-key or native Codex route; credentials are not interchangeable.",
|
|
13
|
+
hardened_oauth_rule_missing: "This OAuth grant does not authorize this operation. Select a separately authenticated supported route; Toolkit will not switch credentials.",
|
|
14
|
+
rejected_by_hardened_oauth_boundary: "This OAuth grant does not authorize this operation. Select a separately authenticated supported route; Toolkit will not switch credentials.",
|
|
15
|
+
missing_scope: "The credential lacks the required Responses authorization scope. Authenticate the intended route separately.",
|
|
16
|
+
invalid_api_key: "The selected route rejected its credential. Check authentication for that route.",
|
|
17
|
+
insufficient_quota: "The selected route has insufficient quota. Check its billing and quota.",
|
|
18
|
+
rate_limit_exceeded: "The selected route is rate limited. Retry later.",
|
|
19
|
+
unsupported_capability: "The endpoint does not support the requested compaction protocol.",
|
|
20
|
+
unsupported_compaction: "The endpoint does not support the requested compaction protocol.",
|
|
21
|
+
unsupported_protocol: "The endpoint does not support the requested compaction protocol.",
|
|
22
|
+
unsupported_parameter: "The endpoint rejected a compaction request parameter.",
|
|
23
|
+
unknown_parameter: "The endpoint rejected a compaction request parameter.",
|
|
24
|
+
invalid_request_error: "The endpoint rejected the compaction request.",
|
|
25
|
+
};
|
|
26
|
+
const PROTOCOL_CODES = new Set(["unsupported_capability", "unsupported_compaction", "unsupported_protocol", "unsupported_parameter", "unknown_parameter"]);
|
|
27
|
+
const SAFE_TYPES = new Set(["invalid_request_error", "authentication_error", "permission_error", "rate_limit_error", "insufficient_quota", "server_error"]);
|
|
28
|
+
const SAFE_PARAMS = new Set(["input", "model", "compaction_trigger", "stream", "store", "tools", "instructions", "reasoning", "text"]);
|
|
29
|
+
|
|
30
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
31
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function decodeCompactionError(value: unknown): CompactionErrorInfo {
|
|
35
|
+
if (!isRecord(value)) return {};
|
|
36
|
+
const source = isRecord(value.error) ? value.error
|
|
37
|
+
: isRecord(value.response) && isRecord(value.response.error) ? value.response.error : value;
|
|
38
|
+
// Unknown identifiers may themselves contain secrets. Only public vocabulary
|
|
39
|
+
// is emitted; classification can conservatively exclude an unknown auth code.
|
|
40
|
+
const code = typeof source.code === "string" && source.code.length <= 128 ? source.code : undefined;
|
|
41
|
+
const type = typeof source.type === "string" && source.type.length <= 128 ? source.type : undefined;
|
|
42
|
+
const excluded = [code, type].filter((item): item is string => !!item).join(" ");
|
|
43
|
+
const missingScope = typeof source.message === "string" && source.message.length <= 4096 && /missing (?:required )?scopes?\b/i.test(source.message);
|
|
44
|
+
const errorCategory = missingScope || /auth|oauth|permission|subscription|scope|access_denied|forbidden|api_key|token|credential|invalid_grant/i.test(excluded) ? "auth"
|
|
45
|
+
: /quota|billing|rate_limit|too_many_requests/i.test(excluded) ? "quota"
|
|
46
|
+
: code && PROTOCOL_CODES.has(code) ? "protocol" : undefined;
|
|
47
|
+
const errorCode = code && Object.hasOwn(HINTS, code) ? code : undefined;
|
|
48
|
+
return {
|
|
49
|
+
...(errorCode ? { errorCode } : {}),
|
|
50
|
+
...(type && SAFE_TYPES.has(type) ? { errorType: type } : {}),
|
|
51
|
+
...(typeof source.param === "string" && SAFE_PARAMS.has(source.param) ? { errorParam: source.param } : {}),
|
|
52
|
+
...(missingScope ? { errorMessage: HINTS.missing_scope } : errorCode ? { errorMessage: HINTS[errorCode] } : {}),
|
|
53
|
+
...(errorCategory ? { errorCategory } : {}),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** One protocol probe, not a transport/auth retry. Unknown failures fail closed. */
|
|
58
|
+
export function qualifiesForStandaloneCompaction(failure: CompactionErrorInfo & { reason: string; status?: number }): boolean {
|
|
59
|
+
if (failure.errorCategory === "auth" || failure.errorCategory === "quota") return false;
|
|
60
|
+
if (failure.reason === "non-2xx") return [400, 404, 405, 422, 501].includes(failure.status ?? 0);
|
|
61
|
+
if (failure.reason === "error-event") return failure.errorCategory === "protocol";
|
|
62
|
+
return failure.status !== undefined && failure.status >= 200 && failure.status < 300 && [
|
|
63
|
+
"empty-body", "invalid-sse", "missing-completed-event", "incomplete-response",
|
|
64
|
+
"invalid-compaction-count", "malformed-compaction-item", "conflicting-compaction-item",
|
|
65
|
+
"invalid-compaction-metadata", "invalid-event-order", "duplicate-completed-event",
|
|
66
|
+
].includes(failure.reason);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Body collection must be cancellable even with a stalled mocked/custom transport. */
|
|
70
|
+
export async function readCompactionResponseText(response: Response, signal?: AbortSignal): Promise<string> {
|
|
71
|
+
if (signal?.aborted) throw new DOMException("Compaction aborted", "AbortError");
|
|
72
|
+
const reader = response.body?.getReader();
|
|
73
|
+
if (!reader) return "";
|
|
74
|
+
const decoder = new TextDecoder();
|
|
75
|
+
const parts: string[] = [];
|
|
76
|
+
let onAbort: (() => void) | undefined;
|
|
77
|
+
const aborted = new Promise<never>((_resolve, reject) => {
|
|
78
|
+
onAbort = () => reject(new DOMException("Compaction aborted", "AbortError"));
|
|
79
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
80
|
+
});
|
|
81
|
+
try {
|
|
82
|
+
while (true) {
|
|
83
|
+
if (signal?.aborted) throw new DOMException("Compaction aborted", "AbortError");
|
|
84
|
+
const chunk = await Promise.race([reader.read(), aborted]);
|
|
85
|
+
if (signal?.aborted) throw new DOMException("Compaction aborted", "AbortError");
|
|
86
|
+
parts.push(chunk.done ? decoder.decode() : decoder.decode(chunk.value, { stream: true }));
|
|
87
|
+
if (chunk.done) return parts.join("");
|
|
88
|
+
}
|
|
89
|
+
} finally {
|
|
90
|
+
if (onAbort) signal?.removeEventListener("abort", onAbort);
|
|
91
|
+
// Abort/completion never waits for transport cleanup. Cancel the reader,
|
|
92
|
+
// not the locked body, and observe late rejection before releasing it.
|
|
93
|
+
try { void reader.cancel().catch(() => undefined); } catch { /* Custom transport cleanup. */ }
|
|
94
|
+
finally { try { reader.releaseLock(); } catch { /* Preserve the operation outcome. */ } }
|
|
95
|
+
}
|
|
96
|
+
}
|