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.
@@ -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 { ArtifactContext, CompactionConfig } from "./types";
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
- /** Assistant summary text extracted from the compact output, for CompactionEntry.summary. */
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(isCompactOutputItem);
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: NativeCompactionRequestBody = {
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.text();
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 (error) {
259
+ } catch {
249
260
  const failure: NativeCompactionClientFailure = {
250
261
  ok: false,
251
262
  reason: "invalid-json",
252
263
  status: response.status,
253
- errorMessage: error instanceof Error ? error.message : String(error),
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: [...parsed.output],
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: error instanceof Error ? error.message : String(error),
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
+ }