@criogaid/pi-codex-compaction 0.4.1 → 0.5.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/CHANGELOG.md +18 -0
- package/README.md +23 -30
- package/package.json +7 -5
- package/src/fallback-settings.ts +59 -25
- package/src/index.ts +43 -38
- package/src/protocol.ts +29 -1
- package/src/remote-retry.ts +277 -0
- package/src/remote.ts +142 -69
- package/src/request-snapshot.ts +109 -36
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
// Own bounded V2 retry evidence before Pi adapters reduce errors to message strings.
|
|
2
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
3
|
+
import { isObject } from "./protocol.js";
|
|
4
|
+
|
|
5
|
+
const MAX_RETRIES = 2;
|
|
6
|
+
const MAX_RETRY_DELAY_MS = 60_000;
|
|
7
|
+
const MAX_OBSERVED_EVENT_CHARS = 1_048_576;
|
|
8
|
+
const TRANSIENT_HTTP = new Set([408, 409, 429, 500, 502, 503, 504]);
|
|
9
|
+
const TRANSIENT_CODES = new Set(["server_error", "server_is_overloaded", "rate_limit_exceeded", "slow_down"]);
|
|
10
|
+
const PERMANENT_CODES = new Set([
|
|
11
|
+
"insufficient_quota", "credit_balance_exhausted", "organization_spend_limit_exceeded",
|
|
12
|
+
"project_spend_limit_exceeded", "usage_limit_reached", "usage_not_included", "context_length_exceeded",
|
|
13
|
+
"invalid_prompt", "invalid_api_key", "authentication_error", "permission_denied", "access_denied",
|
|
14
|
+
"cyber_policy", "bio_policy", "misalignment_policy_violation", "content_filter",
|
|
15
|
+
]);
|
|
16
|
+
const TRANSIENT_NETWORK = new Set([
|
|
17
|
+
"ECONNRESET", "ECONNREFUSED", "EPIPE", "ETIMEDOUT", "EAI_AGAIN", "ENETUNREACH", "EHOSTUNREACH",
|
|
18
|
+
"UND_ERR_SOCKET", "UND_ERR_CONNECT_TIMEOUT", "UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export interface RetryEvidence { reason: string; retryAt?: number }
|
|
22
|
+
|
|
23
|
+
export function compactionRetryLimit(configured?: number): number {
|
|
24
|
+
return Math.max(0, Math.min(MAX_RETRIES, Math.floor(Number.isFinite(configured) ? configured! : MAX_RETRIES)));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function retryDeadline(headers: Headers): number | undefined {
|
|
28
|
+
const milliseconds = headers.get("retry-after-ms");
|
|
29
|
+
const seconds = headers.get("retry-after");
|
|
30
|
+
for (const [value, scale] of [[milliseconds, 1], [seconds, 1_000]] as const) {
|
|
31
|
+
if (value !== null && /^\d+(?:\.\d+)?$/.test(value.trim())) {
|
|
32
|
+
const ms = Number(value) * scale;
|
|
33
|
+
// Numeric overflow is still server advice beyond our cap, not an absent header.
|
|
34
|
+
return Date.now() + ms;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (seconds !== null && !/^[+-]?[\d.]+$/.test(seconds.trim())) {
|
|
38
|
+
const date = Date.parse(seconds);
|
|
39
|
+
if (Number.isFinite(date)) return Math.max(Date.now(), date);
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function waitForCompactionRetry(
|
|
45
|
+
evidence: RetryEvidence, retry: number, maxDelayMs: number | undefined, signal: AbortSignal,
|
|
46
|
+
): Promise<void> {
|
|
47
|
+
signal.throwIfAborted();
|
|
48
|
+
// Pi's zero means no configured ceiling; V2 still enforces its own bounded wait.
|
|
49
|
+
const cap = maxDelayMs !== undefined && Number.isFinite(maxDelayMs) && maxDelayMs > 0
|
|
50
|
+
? Math.min(maxDelayMs, MAX_RETRY_DELAY_MS) : MAX_RETRY_DELAY_MS;
|
|
51
|
+
const requested = evidence.retryAt === undefined ? undefined : Math.max(0, evidence.retryAt - Date.now());
|
|
52
|
+
if (requested !== undefined && requested > cap) {
|
|
53
|
+
throw new Error(`Remote compaction retry delay exceeds the allowed maximum of ${cap}ms (${evidence.reason})`);
|
|
54
|
+
}
|
|
55
|
+
await delay(requested ?? Math.min(500 * 2 ** retry, cap), undefined, { signal });
|
|
56
|
+
signal.throwIfAborted();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function networkFailure(error: unknown): boolean {
|
|
60
|
+
const visited = new Set<unknown>();
|
|
61
|
+
for (let current = error; isObject(current) && !visited.has(current); current = current.cause) {
|
|
62
|
+
visited.add(current);
|
|
63
|
+
if (typeof current.code === "string" && TRANSIENT_NETWORK.has(current.code)) return true;
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function classifyError(value: unknown): { code?: string; permanent: boolean; invalid: boolean; message?: string } {
|
|
69
|
+
if (!isObject(value)) return { permanent: false, invalid: true };
|
|
70
|
+
const records = [value, ...(isObject(value.error) ? [value.error] : [])];
|
|
71
|
+
let invalid = value.error !== undefined && !isObject(value.error);
|
|
72
|
+
const codes: string[] = [];
|
|
73
|
+
for (const record of records) {
|
|
74
|
+
if (record.code !== undefined) {
|
|
75
|
+
if (typeof record.code === "string" && record.code) codes.push(record.code);
|
|
76
|
+
else invalid = true;
|
|
77
|
+
} else if (typeof record.type === "string" && record.type !== "error" &&
|
|
78
|
+
!record.type.startsWith("response.")) codes.push(record.type);
|
|
79
|
+
}
|
|
80
|
+
const unique = [...new Set(codes)];
|
|
81
|
+
return {
|
|
82
|
+
code: unique.length === 1 ? unique[0] : undefined,
|
|
83
|
+
permanent: codes.some((code) => PERMANENT_CODES.has(code)),
|
|
84
|
+
invalid: invalid || unique.length > 1,
|
|
85
|
+
message: records.map((record) => record.message).find((message): message is string => typeof message === "string"),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function rateLimitDeadline(code: string, message: string | undefined): number | undefined {
|
|
90
|
+
// Codex interprets this advice only for explicit rate-limit codes, never arbitrary error prose.
|
|
91
|
+
if (code !== "rate_limit_exceeded" && code !== "slow_down") return undefined;
|
|
92
|
+
const match = message?.match(/\btry again in\s+(\d+(?:\.\d+)?)\s*(ms|seconds?|s)\b/i);
|
|
93
|
+
if (!match) return undefined;
|
|
94
|
+
const ms = Number(match[1]) * (match[2].toLowerCase() === "ms" ? 1 : 1_000);
|
|
95
|
+
return Date.now() + ms;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Observe framing independently: OpenAI's SDK intercepts some SSE error frames before Pi's raw hook. */
|
|
99
|
+
class SseEvidenceObserver {
|
|
100
|
+
private line = "";
|
|
101
|
+
private data: string[] = [];
|
|
102
|
+
private size = 0;
|
|
103
|
+
private event = "";
|
|
104
|
+
private skipLf = false;
|
|
105
|
+
private oversized = false;
|
|
106
|
+
constructor(private readonly attempt: CompactionAttempt) {}
|
|
107
|
+
feed(text: string): void {
|
|
108
|
+
if (this.oversized) return;
|
|
109
|
+
for (const char of text) {
|
|
110
|
+
if (this.skipLf && char === "\n") { this.skipLf = false; continue; }
|
|
111
|
+
this.skipLf = false;
|
|
112
|
+
if (char === "\r" || char === "\n") {
|
|
113
|
+
this.finishLine();
|
|
114
|
+
this.skipLf = char === "\r";
|
|
115
|
+
} else if (!this.oversized) {
|
|
116
|
+
this.size += char.length;
|
|
117
|
+
if (this.size > MAX_OBSERVED_EVENT_CHARS) {
|
|
118
|
+
this.oversized = true;
|
|
119
|
+
this.line = "";
|
|
120
|
+
this.data = [];
|
|
121
|
+
this.attempt.blockRetry();
|
|
122
|
+
return;
|
|
123
|
+
} else this.line += char;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
private finishLine(): void {
|
|
128
|
+
const line = this.line;
|
|
129
|
+
this.line = "";
|
|
130
|
+
if (this.oversized) return;
|
|
131
|
+
if (!line) {
|
|
132
|
+
if (this.data.length) {
|
|
133
|
+
const data = this.data.join("\n");
|
|
134
|
+
if (data.trim() === "[DONE]") this.attempt.blockRetry();
|
|
135
|
+
else {
|
|
136
|
+
try { this.attempt.observeEvent(JSON.parse(data), this.event); }
|
|
137
|
+
catch { this.attempt.blockRetry(); }
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
this.data = [];
|
|
141
|
+
this.event = "";
|
|
142
|
+
this.size = 0;
|
|
143
|
+
} else if (!line.startsWith(":")) {
|
|
144
|
+
const separator = line.indexOf(":");
|
|
145
|
+
const field = separator < 0 ? line : line.slice(0, separator);
|
|
146
|
+
const value = separator < 0 ? "" : line.slice(separator + 1).replace(/^ /, "");
|
|
147
|
+
if (field === "data") this.data.push(value);
|
|
148
|
+
if (field === "event") this.event = value;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
finish(): void {
|
|
152
|
+
if (this.oversized) return;
|
|
153
|
+
if (this.line) this.finishLine();
|
|
154
|
+
if (this.data.length) {
|
|
155
|
+
// EOF can cut a valid response mid-frame. Still veto a complete permanent error at EOF.
|
|
156
|
+
try { this.attempt.observeEvent(JSON.parse(this.data.join("\n")), this.event); }
|
|
157
|
+
catch { /* Incomplete JSON at EOF is a truncated frame, unlike invalid JSON in a complete frame. */ }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export class CompactionAttempt {
|
|
163
|
+
private evidence?: RetryEvidence;
|
|
164
|
+
private blocked = false;
|
|
165
|
+
private terminal = false;
|
|
166
|
+
private progress = false;
|
|
167
|
+
private eof = false;
|
|
168
|
+
fatal?: { error: unknown };
|
|
169
|
+
|
|
170
|
+
blockRetry(error?: unknown): void {
|
|
171
|
+
this.blocked = true;
|
|
172
|
+
if (error !== undefined && !this.fatal) this.fatal = { error };
|
|
173
|
+
}
|
|
174
|
+
fetchFailed(error: unknown): void {
|
|
175
|
+
if (networkFailure(error)) this.evidence = { reason: "transient network failure" };
|
|
176
|
+
}
|
|
177
|
+
observeEvent(value: unknown, eventName?: string): Error | undefined {
|
|
178
|
+
if (!isObject(value)) { this.blockRetry(); return; }
|
|
179
|
+
const response = isObject(value.response) ? value.response : undefined;
|
|
180
|
+
if (eventName === "error" || value.type === "error" || isObject(value.error) || value.type === "response.failed") {
|
|
181
|
+
const error = classifyError(value.type === "response.failed" ? response?.error : value);
|
|
182
|
+
const code = error.code;
|
|
183
|
+
if (!error.invalid && !error.permanent && code && TRANSIENT_CODES.has(code)) {
|
|
184
|
+
this.evidence = { reason: `server error ${code}`, retryAt: rateLimitDeadline(code, error.message) };
|
|
185
|
+
return new Error(`Remote compaction failed temporarily (${code})`);
|
|
186
|
+
}
|
|
187
|
+
this.blockRetry();
|
|
188
|
+
}
|
|
189
|
+
if (typeof value.type !== "string") this.blockRetry();
|
|
190
|
+
if (value.type === "response.completed" || value.type === "response.done" ||
|
|
191
|
+
value.type === "response.incomplete" || value.type === "response.failed" || value.type === "response.cancelled") {
|
|
192
|
+
this.terminal = true;
|
|
193
|
+
}
|
|
194
|
+
if (value.type === "response.created" || value.type === "response.in_progress" ||
|
|
195
|
+
value.type === "response.output_item.added" || value.type === "response.output_item.done") this.progress = true;
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
retryEvidence(): RetryEvidence | undefined {
|
|
199
|
+
if (this.blocked || this.terminal) return undefined;
|
|
200
|
+
return this.evidence ?? (this.eof && this.progress ? { reason: "Responses stream ended before its terminal event" } : undefined);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
observeResponse(response: Response, signal: AbortSignal): Response {
|
|
204
|
+
const isSse = response.ok && response.headers.get("content-type")?.toLowerCase().includes("text/event-stream");
|
|
205
|
+
if (!response.ok) {
|
|
206
|
+
if (TRANSIENT_HTTP.has(response.status) && response.headers.get("x-should-retry") !== "false") {
|
|
207
|
+
this.evidence = { reason: `HTTP ${response.status}`, retryAt: retryDeadline(response.headers) };
|
|
208
|
+
} else this.blockRetry();
|
|
209
|
+
}
|
|
210
|
+
if (!response.body || (response.ok && !isSse)) return response;
|
|
211
|
+
const reader = response.body.getReader();
|
|
212
|
+
const decoder = new TextDecoder();
|
|
213
|
+
const sse = isSse ? new SseEvidenceObserver(this) : undefined;
|
|
214
|
+
let errorBody = "";
|
|
215
|
+
let errorBodyTooLarge = false;
|
|
216
|
+
let closed = false;
|
|
217
|
+
let abort: () => void;
|
|
218
|
+
const cleanup = () => { closed = true; signal.removeEventListener("abort", abort); };
|
|
219
|
+
const inspect = (text: string) => {
|
|
220
|
+
if (sse) sse.feed(text);
|
|
221
|
+
else if (!errorBodyTooLarge) {
|
|
222
|
+
if (errorBody.length + text.length > MAX_OBSERVED_EVENT_CHARS) {
|
|
223
|
+
errorBodyTooLarge = true;
|
|
224
|
+
errorBody = "";
|
|
225
|
+
this.blockRetry();
|
|
226
|
+
} else errorBody += text;
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
const body = new ReadableStream<Uint8Array>({
|
|
230
|
+
start(controller) {
|
|
231
|
+
abort = () => {
|
|
232
|
+
if (closed) return;
|
|
233
|
+
cleanup();
|
|
234
|
+
controller.error(signal.reason);
|
|
235
|
+
void reader.cancel(signal.reason).finally(() => reader.releaseLock()).catch(() => {});
|
|
236
|
+
};
|
|
237
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
238
|
+
if (signal.aborted) abort();
|
|
239
|
+
},
|
|
240
|
+
pull: async (controller) => {
|
|
241
|
+
try {
|
|
242
|
+
const result = await reader.read();
|
|
243
|
+
if (closed) return;
|
|
244
|
+
if (result.done) {
|
|
245
|
+
inspect(decoder.decode());
|
|
246
|
+
sse?.finish();
|
|
247
|
+
if (!sse && !errorBodyTooLarge) {
|
|
248
|
+
try {
|
|
249
|
+
const error = classifyError(JSON.parse(errorBody));
|
|
250
|
+
if (error.permanent || error.invalid || (error.code && !TRANSIENT_CODES.has(error.code))) this.blockRetry();
|
|
251
|
+
} catch { /* A transient HTTP status can legitimately carry a non-JSON gateway error. */ }
|
|
252
|
+
}
|
|
253
|
+
this.eof = Boolean(sse);
|
|
254
|
+
cleanup();
|
|
255
|
+
reader.releaseLock();
|
|
256
|
+
controller.close();
|
|
257
|
+
} else {
|
|
258
|
+
inspect(decoder.decode(result.value, { stream: true }));
|
|
259
|
+
controller.enqueue(result.value);
|
|
260
|
+
}
|
|
261
|
+
} catch (error) {
|
|
262
|
+
if (closed) return;
|
|
263
|
+
this.fetchFailed(error);
|
|
264
|
+
cleanup();
|
|
265
|
+
reader.releaseLock();
|
|
266
|
+
controller.error(error);
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
cancel(reason) {
|
|
270
|
+
if (closed) return;
|
|
271
|
+
cleanup();
|
|
272
|
+
return reader.cancel(reason).finally(() => reader.releaseLock());
|
|
273
|
+
},
|
|
274
|
+
}, { highWaterMark: 0 });
|
|
275
|
+
return new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
|
|
276
|
+
}
|
|
277
|
+
}
|
package/src/remote.ts
CHANGED
|
@@ -6,12 +6,12 @@ import { capableModel, deriveEndpoint, normalizeUrl, sameBackend, sameModel, typ
|
|
|
6
6
|
import { trimToolOutputsToContextWindow } from "./context-window.js";
|
|
7
7
|
import { estimateImages, type ImageEstimates } from "./image-budget.js";
|
|
8
8
|
import { contextUserItems, type UserItemOrigin } from "./retention-input.js";
|
|
9
|
-
import { CodexCompactionProtocolError, createCompactionCollector, isObject, type JsonObject, prepareRemoteCompactionPayload } from "./protocol.js";
|
|
9
|
+
import { CodexCompactionProtocolError, createCompactionCollector, isObject, type JsonObject, prepareRemoteCompactionPayload, withoutInputImages } from "./protocol.js";
|
|
10
10
|
import { applyProviderRequest, type ProviderRequestSnapshot } from "./request-snapshot.js";
|
|
11
|
+
import { CompactionAttempt, compactionRetryLimit, waitForCompactionRetry } from "./remote-retry.js";
|
|
11
12
|
|
|
12
13
|
const REMOTE_COMPACTION_FEATURE = "remote_compaction_v2";
|
|
13
14
|
const REQUEST_TIMEOUT_MS = 300_000;
|
|
14
|
-
const MAX_RETRIES = 2;
|
|
15
15
|
const MISSING_PAYLOAD_MESSAGE = "Provider did not expose a request payload";
|
|
16
16
|
|
|
17
17
|
export interface RemoteCompactionRequest {
|
|
@@ -21,6 +21,8 @@ export interface RemoteCompactionRequest {
|
|
|
21
21
|
reasoning: ThinkingLevel;
|
|
22
22
|
sessionId: string;
|
|
23
23
|
thinkingBudgets?: ThinkingBudgets;
|
|
24
|
+
/** Apply the current Pi image policy to both fresh input and replayed opaque checkpoint history. */
|
|
25
|
+
blockImages?: boolean;
|
|
24
26
|
/** Pi's provider retry setting; Codex caps compaction retries below it. */
|
|
25
27
|
maxRetries?: number;
|
|
26
28
|
maxRetryDelayMs?: number;
|
|
@@ -63,81 +65,152 @@ function objectItems(input: unknown): JsonObject[] {
|
|
|
63
65
|
|
|
64
66
|
export async function requestRemoteCompaction(request: RemoteCompactionRequest): Promise<RemoteCompactionResponse> {
|
|
65
67
|
request.signal.throwIfAborted();
|
|
66
|
-
const
|
|
68
|
+
const model = structuredClone(request.model);
|
|
69
|
+
const context = structuredClone(request.context);
|
|
70
|
+
const configured = capableModel(model);
|
|
67
71
|
if (!configured) throw new CodexCompactionProtocolError("Model is not configured for remote compaction");
|
|
68
|
-
const
|
|
72
|
+
const retries = compactionRetryLimit(request.maxRetries);
|
|
73
|
+
let preparedPayload: JsonObject | undefined;
|
|
69
74
|
let sentInput: JsonObject[] | undefined;
|
|
70
75
|
let identity: ProviderIdentity | undefined;
|
|
71
|
-
let usage: Usage | undefined;
|
|
72
76
|
let images: ImageEstimates | undefined;
|
|
73
77
|
let contextual: boolean[] | undefined;
|
|
74
78
|
const baseFetch = request.fetch ?? globalThis.fetch;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
maxRetries: Math.min(request.maxRetries ?? MAX_RETRIES, MAX_RETRIES),
|
|
99
|
-
maxRetryDelayMs: request.maxRetryDelayMs,
|
|
100
|
-
transformHeaders: mergeRemoteCompactionHeader,
|
|
101
|
-
fetch: routedFetch,
|
|
102
|
-
onPayload: async (payload, preparedModel) => {
|
|
103
|
-
if (!sameModel(configured.identity, preparedModel)) {
|
|
104
|
-
throw new CodexCompactionProtocolError("Provider resolved an unexpected compaction model");
|
|
79
|
+
for (let retry = 0; ; retry++) {
|
|
80
|
+
request.signal.throwIfAborted();
|
|
81
|
+
const attempt = new CompactionAttempt();
|
|
82
|
+
const collector = createCompactionCollector();
|
|
83
|
+
let usage: Usage | undefined;
|
|
84
|
+
let compactionCount = 0;
|
|
85
|
+
let fetched = false;
|
|
86
|
+
const routedFetch: typeof globalThis.fetch = async (input, init) => {
|
|
87
|
+
try {
|
|
88
|
+
request.signal.throwIfAborted();
|
|
89
|
+
if (!identity) throw new CodexCompactionProtocolError(MISSING_PAYLOAD_MESSAGE);
|
|
90
|
+
const actual = normalizeUrl(input instanceof Request ? input.url : String(input));
|
|
91
|
+
const defaultEndpoint = deriveEndpoint(identity.baseUrl, identity.api);
|
|
92
|
+
if (actual !== defaultEndpoint && actual !== identity.endpoint) {
|
|
93
|
+
throw new CodexCompactionProtocolError(`Provider requested unexpected compaction endpoint ${actual}`);
|
|
94
|
+
}
|
|
95
|
+
const method = init?.method ?? (input instanceof Request ? input.method : "GET");
|
|
96
|
+
if (method.toUpperCase() !== "POST") throw new CodexCompactionProtocolError("Remote compaction request must use POST");
|
|
97
|
+
if (fetched) throw new CodexCompactionProtocolError("Provider exceeded the single-request compaction attempt budget");
|
|
98
|
+
fetched = true;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
attempt.blockRetry(error);
|
|
101
|
+
throw error;
|
|
105
102
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
103
|
+
try {
|
|
104
|
+
// Endpoint overrides are same-origin HTTP routes; authentication remains assembled by Pi.
|
|
105
|
+
const actual = normalizeUrl(input instanceof Request ? input.url : String(input));
|
|
106
|
+
const response = actual === identity!.endpoint ? await baseFetch(input, init)
|
|
107
|
+
: input instanceof Request
|
|
108
|
+
? await baseFetch(new Request(identity!.endpoint, new Request(input, init)))
|
|
109
|
+
: await baseFetch(identity!.endpoint, init);
|
|
110
|
+
return attempt.observeResponse(response, request.signal);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
attempt.fetchFailed(error);
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
try {
|
|
117
|
+
const stream = request.modelRegistry.streamSimple(model, structuredClone(context), {
|
|
118
|
+
signal: request.signal,
|
|
119
|
+
// Pi pools WebSockets without comparing handshake headers; V2 needs its feature header on every request.
|
|
120
|
+
transport: "sse",
|
|
121
|
+
reasoning: request.reasoning === "off" ? undefined : request.reasoning,
|
|
122
|
+
thinkingBudgets: request.thinkingBudgets,
|
|
123
|
+
sessionId: request.sessionId,
|
|
124
|
+
timeoutMs: REQUEST_TIMEOUT_MS,
|
|
125
|
+
// Only the outer V2 loop retries; provider HTTP retries would multiply the global budget.
|
|
126
|
+
maxRetries: 0,
|
|
127
|
+
maxRetryDelayMs: request.maxRetryDelayMs,
|
|
128
|
+
transformHeaders: mergeRemoteCompactionHeader,
|
|
129
|
+
fetch: routedFetch,
|
|
130
|
+
onPayload: async (payload, preparedModel) => {
|
|
131
|
+
try {
|
|
132
|
+
request.signal.throwIfAborted();
|
|
133
|
+
if (!sameModel(configured.identity, preparedModel)) {
|
|
134
|
+
throw new CodexCompactionProtocolError("Provider resolved an unexpected compaction model");
|
|
135
|
+
}
|
|
136
|
+
const resolved = capableModel(model, preparedModel.baseUrl);
|
|
137
|
+
if (!resolved) throw new CodexCompactionProtocolError("Resolved provider endpoint is incompatible with remote compaction");
|
|
138
|
+
if (identity && !sameBackend(identity, resolved.identity)) {
|
|
139
|
+
throw new CodexCompactionProtocolError("Resolved provider backend changed during remote compaction retries");
|
|
140
|
+
}
|
|
141
|
+
identity = resolved.identity;
|
|
142
|
+
const prior = request.priorCheckpoint?.identity;
|
|
143
|
+
if (prior && !sameBackend(prior, identity)) {
|
|
144
|
+
throw new CodexCompactionProtocolError("The active opaque checkpoint belongs to a different resolved provider backend");
|
|
145
|
+
}
|
|
146
|
+
if (preparedPayload) {
|
|
147
|
+
if (!isObject(payload) || payload.model !== model.id) {
|
|
148
|
+
throw new CodexCompactionProtocolError("Provider payload used an unexpected model");
|
|
149
|
+
}
|
|
150
|
+
// Re-authenticate each attempt, but never let a new serialization change the compacted input.
|
|
151
|
+
request.onPrepared?.();
|
|
152
|
+
return structuredClone(preparedPayload);
|
|
153
|
+
}
|
|
154
|
+
if (!isObject(payload)) throw new CodexCompactionProtocolError("Prepared compaction payload must be an object");
|
|
155
|
+
const applied = applyProviderRequest(payload, resolved, request.providerRequest,
|
|
156
|
+
contextUserItems(payload.input, request.userItemOrigins));
|
|
157
|
+
const adapted = request.blockImages
|
|
158
|
+
? { ...applied.payload, input: withoutInputImages(objectItems(applied.payload.input)) } : applied.payload;
|
|
159
|
+
const checkpoint = request.priorCheckpoint && request.blockImages
|
|
160
|
+
? { ...request.priorCheckpoint, replacementHistory: withoutInputImages(request.priorCheckpoint.replacementHistory) }
|
|
161
|
+
: request.priorCheckpoint;
|
|
162
|
+
const payloadItems = objectItems(adapted.input);
|
|
163
|
+
const contextItems = new Set(payloadItems.filter((_, index) => applied.contextual[index]));
|
|
164
|
+
const estimates = await estimateImages([...payloadItems, ...checkpoint?.replacementHistory ?? []], request.signal);
|
|
165
|
+
const prepared = prepareRemoteCompactionPayload(adapted, checkpoint, (history) => ({
|
|
166
|
+
...history,
|
|
167
|
+
input: trimToolOutputsToContextWindow(objectItems(history.input), history.instructions, preparedModel.contextWindow, estimates),
|
|
168
|
+
}));
|
|
169
|
+
if (prepared.model !== model.id) throw new CodexCompactionProtocolError("Provider payload used an unexpected model");
|
|
170
|
+
const sent = objectItems(prepared.input).slice(0, -1);
|
|
171
|
+
contextual = sent.map((item) => contextItems.has(item));
|
|
172
|
+
sentInput = structuredClone(sent);
|
|
173
|
+
images = estimates;
|
|
174
|
+
preparedPayload = structuredClone(prepared);
|
|
175
|
+
request.onPrepared?.();
|
|
176
|
+
return prepared;
|
|
177
|
+
} catch (error) {
|
|
178
|
+
attempt.blockRetry(error);
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
onProviderStreamEvent: (event) => {
|
|
183
|
+
request.signal.throwIfAborted();
|
|
184
|
+
const transient = attempt.observeEvent(event);
|
|
185
|
+
if (transient) throw transient;
|
|
186
|
+
try {
|
|
187
|
+
if (isObject(event) && event.type === "response.output_item.done" && isObject(event.item) &&
|
|
188
|
+
(event.item.type === "compaction" || event.item.type === "compaction_summary") && ++compactionCount > 1) {
|
|
189
|
+
throw new CodexCompactionProtocolError("Remote compaction returned duplicate compaction output events");
|
|
190
|
+
}
|
|
191
|
+
collector.observe(event);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
attempt.blockRetry(error);
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
for await (const event of stream) {
|
|
199
|
+
request.signal.throwIfAborted();
|
|
200
|
+
if (event.type === "error") throw new Error(event.error.errorMessage ?? "Codex compaction request failed");
|
|
201
|
+
if (event.type === "done") usage = event.message.usage;
|
|
112
202
|
}
|
|
113
|
-
if (isObject(payload)) payload = applyProviderRequest(payload, resolved, request.providerRequest);
|
|
114
|
-
const contextItems = contextUserItems(isObject(payload) ? payload.input : undefined, request.userItemOrigins);
|
|
115
|
-
const payloadItems = isObject(payload) && Array.isArray(payload.input) ? payload.input.filter(isObject) : [];
|
|
116
|
-
const estimates = await estimateImages([...payloadItems, ...request.priorCheckpoint?.replacementHistory ?? []], request.signal);
|
|
117
|
-
const prepared = prepareRemoteCompactionPayload(payload, request.priorCheckpoint, (history) => ({
|
|
118
|
-
...history,
|
|
119
|
-
input: trimToolOutputsToContextWindow(objectItems(history.input), history.instructions, request.model.contextWindow, estimates),
|
|
120
|
-
}));
|
|
121
|
-
if (prepared.model !== request.model.id) throw new CodexCompactionProtocolError("Provider payload used an unexpected model");
|
|
122
|
-
const sent = objectItems(prepared.input).slice(0, -1);
|
|
123
|
-
contextual = sent.map((item) => contextItems.has(item));
|
|
124
|
-
sentInput = structuredClone(sent);
|
|
125
|
-
images = estimates;
|
|
126
|
-
request.onPrepared?.();
|
|
127
|
-
return prepared;
|
|
128
|
-
},
|
|
129
|
-
onProviderStreamEvent: (event) => {
|
|
130
203
|
request.signal.throwIfAborted();
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
204
|
+
if (attempt.fatal) throw attempt.fatal.error;
|
|
205
|
+
if (!sentInput || !identity || !images || !contextual) throw new CodexCompactionProtocolError(MISSING_PAYLOAD_MESSAGE);
|
|
206
|
+
if (!usage) throw new CodexCompactionProtocolError("Provider stream ended without a completed message");
|
|
207
|
+
return { item: collector.finish(), promptInput: sentInput, identity, usage, images, contextual };
|
|
208
|
+
} catch (error) {
|
|
209
|
+
request.signal.throwIfAborted();
|
|
210
|
+
if (attempt.fatal) throw attempt.fatal.error;
|
|
211
|
+
const evidence = attempt.retryEvidence();
|
|
212
|
+
if (!evidence || retry >= retries) throw error;
|
|
213
|
+
await waitForCompactionRetry(evidence, retry, request.maxRetryDelayMs, request.signal);
|
|
214
|
+
}
|
|
138
215
|
}
|
|
139
|
-
request.signal.throwIfAborted();
|
|
140
|
-
if (!sentInput || !identity || !images || !contextual) throw new CodexCompactionProtocolError(MISSING_PAYLOAD_MESSAGE);
|
|
141
|
-
if (!usage) throw new CodexCompactionProtocolError("Provider stream ended without a completed message");
|
|
142
|
-
return { item: collector.finish(), promptInput: sentInput, identity, usage, images, contextual };
|
|
143
216
|
}
|