codex-copilot-dx 0.7.6 → 0.7.8

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.
@@ -0,0 +1,268 @@
1
+ import dns from "node:dns/promises";
2
+ import https from "node:https";
3
+ import net from "node:net";
4
+
5
+ export const IMAGE_MAX_BYTES = 32 * 1024 * 1024;
6
+ export const IMAGE_GENERATION_TIMEOUT_MS = 180_000;
7
+ export const IMAGE_DOWNLOAD_TIMEOUT_MS = 60_000;
8
+ export const IMAGE_SIZES = new Set(["1024x1024", "1536x1024", "1024x1536"]);
9
+
10
+ function providerError(message, code = "ccdx_image_provider_error") {
11
+ const error = new Error(message);
12
+ error.code = code;
13
+ return error;
14
+ }
15
+
16
+ function checkedPrompt(value) {
17
+ const prompt = String(value || "").trim();
18
+ if (!prompt) throw providerError("Image prompt is required", "ccdx_image_prompt_required");
19
+ if (prompt.length > 16_000) throw providerError("Image prompt exceeds 16000 characters", "ccdx_image_prompt_too_large");
20
+ return prompt;
21
+ }
22
+
23
+ function checkedSize(value) {
24
+ const size = String(value || "1024x1024").trim().toLowerCase();
25
+ if (!IMAGE_SIZES.has(size)) {
26
+ throw providerError(`Image size must be one of: ${[...IMAGE_SIZES].join(", ")}`, "ccdx_image_size_invalid");
27
+ }
28
+ return size;
29
+ }
30
+
31
+ function generationBody(config, prompt, size) {
32
+ if (config.protocol === "qwen-messages") {
33
+ return {
34
+ model: config.model,
35
+ input: { messages: [{ role: "user", content: [{ text: prompt }] }] },
36
+ parameters: { n: 1, size: size.replace("x", "*"), watermark: false },
37
+ };
38
+ }
39
+ return { model: config.model, prompt, n: 1, size };
40
+ }
41
+
42
+ async function boundedJson(response) {
43
+ const maxBytes = 48 * 1024 * 1024;
44
+ const declared = Number(response.headers?.get?.("content-length"));
45
+ if (Number.isFinite(declared) && declared > maxBytes) throw providerError("Image API response was too large");
46
+ const reader = response.body?.getReader?.();
47
+ let text;
48
+ if (!reader) {
49
+ text = await response.text();
50
+ if (Buffer.byteLength(text) > maxBytes) throw providerError("Image API response was too large");
51
+ } else {
52
+ const chunks = [];
53
+ let total = 0;
54
+ try {
55
+ while (true) {
56
+ const { done, value } = await reader.read();
57
+ if (done) break;
58
+ total += value.byteLength;
59
+ if (total > maxBytes) {
60
+ await reader.cancel().catch(() => {});
61
+ throw providerError("Image API response was too large");
62
+ }
63
+ chunks.push(Buffer.from(value.buffer, value.byteOffset, value.byteLength));
64
+ }
65
+ } finally {
66
+ reader.releaseLock();
67
+ }
68
+ text = Buffer.concat(chunks, total).toString("utf8");
69
+ }
70
+ try { return JSON.parse(text); } catch { throw providerError("Image API returned invalid JSON"); }
71
+ }
72
+
73
+ function upstreamMessage(body) {
74
+ return String(body?.message || body?.error?.message || body?.error || "")
75
+ .replace(/[\u0000-\u001f\u007f-\u009f]/g, " ")
76
+ .trim()
77
+ .slice(0, 500);
78
+ }
79
+
80
+ function imageSource(body) {
81
+ const standard = body?.data?.[0];
82
+ if (typeof standard?.b64_json === "string" && standard.b64_json) {
83
+ return { type: "base64", value: standard.b64_json };
84
+ }
85
+ if (typeof standard?.url === "string" && standard.url) return { type: "url", value: standard.url };
86
+ const contents = body?.output?.choices?.[0]?.message?.content;
87
+ if (Array.isArray(contents)) {
88
+ const image = contents.find((part) => typeof part?.image === "string" && part.image);
89
+ if (image) return { type: "url", value: image.image };
90
+ }
91
+ return null;
92
+ }
93
+
94
+ function imageMetadata(bytes) {
95
+ if (bytes.length >= 24 && bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) {
96
+ return { mimeType: "image/png", width: bytes.readUInt32BE(16), height: bytes.readUInt32BE(20) };
97
+ }
98
+ if (bytes.length >= 12 && bytes.toString("ascii", 0, 4) === "RIFF" && bytes.toString("ascii", 8, 12) === "WEBP") {
99
+ return { mimeType: "image/webp" };
100
+ }
101
+ if (bytes.length >= 4 && bytes[0] === 0xff && bytes[1] === 0xd8) {
102
+ let offset = 2;
103
+ while (offset + 9 < bytes.length) {
104
+ if (bytes[offset] !== 0xff) { offset += 1; continue; }
105
+ const marker = bytes[offset + 1];
106
+ if (marker === 0xd8 || marker === 0xd9) { offset += 2; continue; }
107
+ const length = bytes.readUInt16BE(offset + 2);
108
+ if (length < 2 || offset + 2 + length > bytes.length) break;
109
+ if ([0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf].includes(marker)) {
110
+ return { mimeType: "image/jpeg", height: bytes.readUInt16BE(offset + 5), width: bytes.readUInt16BE(offset + 7) };
111
+ }
112
+ offset += 2 + length;
113
+ }
114
+ return { mimeType: "image/jpeg" };
115
+ }
116
+ throw providerError("Image API returned an unsupported image format", "ccdx_image_format_unsupported");
117
+ }
118
+
119
+ function ipv4Public(address) {
120
+ const parts = address.split(".").map(Number);
121
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return false;
122
+ const [a, b, c] = parts;
123
+ if (a === 0 || a === 10 || a === 127 || a >= 224) return false;
124
+ if (a === 100 && b >= 64 && b <= 127) return false;
125
+ if (a === 169 && b === 254) return false;
126
+ if (a === 172 && b >= 16 && b <= 31) return false;
127
+ if (a === 192 && (b === 168 || (b === 0 && [0, 2].includes(c)))) return false;
128
+ if (a === 198 && (b === 18 || b === 19 || (b === 51 && c === 100))) return false;
129
+ if (a === 203 && b === 0 && c === 113) return false;
130
+ return true;
131
+ }
132
+
133
+ function publicAddress(address) {
134
+ const family = net.isIP(address);
135
+ if (family === 4) return ipv4Public(address);
136
+ if (family !== 6) return false;
137
+ const lower = address.toLowerCase();
138
+ if (lower === "::" || lower === "::1" || lower.startsWith("fc") || lower.startsWith("fd")
139
+ || /^fe[89ab]/.test(lower) || lower.startsWith("ff") || lower.startsWith("2001:db8:")) return false;
140
+ const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(lower);
141
+ return mapped ? ipv4Public(mapped[1]) : true;
142
+ }
143
+
144
+ export async function downloadPublicImage(urlValue, {
145
+ lookup = dns.lookup,
146
+ timeoutMs = IMAGE_DOWNLOAD_TIMEOUT_MS,
147
+ maxBytes = IMAGE_MAX_BYTES,
148
+ signal,
149
+ } = {}) {
150
+ let url;
151
+ try { url = new URL(urlValue); } catch { throw providerError("Image API returned an invalid image URL"); }
152
+ if (url.protocol !== "https:" || url.username || url.password) {
153
+ throw providerError("Image API returned an unsafe image URL", "ccdx_image_url_unsafe");
154
+ }
155
+ const addresses = await lookup(url.hostname, { all: true, verbatim: true });
156
+ if (!Array.isArray(addresses) || !addresses.length || addresses.some(({ address }) => !publicAddress(address))) {
157
+ throw providerError("Image API returned a non-public image URL", "ccdx_image_url_unsafe");
158
+ }
159
+ return new Promise((resolve, reject) => {
160
+ let settled = false;
161
+ const finish = (error, value) => {
162
+ if (settled) return;
163
+ settled = true;
164
+ signal?.removeEventListener("abort", onAbort);
165
+ if (error) reject(error);
166
+ else resolve(value);
167
+ };
168
+ const request = https.get(url, {
169
+ headers: { Accept: "image/*,application/octet-stream;q=0.5" },
170
+ lookup: (_hostname, options, callback) => {
171
+ if (options?.all) callback(null, addresses);
172
+ else callback(null, addresses[0].address, addresses[0].family);
173
+ },
174
+ }, (response) => {
175
+ if (response.statusCode < 200 || response.statusCode >= 300) {
176
+ response.resume();
177
+ finish(providerError(`Image download failed with HTTP ${response.statusCode}`));
178
+ return;
179
+ }
180
+ const declared = Number(response.headers["content-length"]);
181
+ if (Number.isFinite(declared) && declared > maxBytes) {
182
+ response.destroy();
183
+ finish(providerError("Generated image exceeds the size limit", "ccdx_image_too_large"));
184
+ return;
185
+ }
186
+ const chunks = [];
187
+ let total = 0;
188
+ response.on("data", (chunk) => {
189
+ total += chunk.length;
190
+ if (total > maxBytes) {
191
+ response.destroy();
192
+ finish(providerError("Generated image exceeds the size limit", "ccdx_image_too_large"));
193
+ return;
194
+ }
195
+ chunks.push(chunk);
196
+ });
197
+ response.on("end", () => finish(null, Buffer.concat(chunks, total)));
198
+ response.on("error", (error) => finish(error));
199
+ });
200
+ const timer = setTimeout(() => request.destroy(providerError("Image download timed out", "ccdx_image_timeout")), timeoutMs);
201
+ timer.unref?.();
202
+ request.on("close", () => clearTimeout(timer));
203
+ request.on("error", (error) => finish(error));
204
+ const onAbort = () => request.destroy(signal.reason instanceof Error ? signal.reason : providerError("Image request was cancelled"));
205
+ if (signal?.aborted) onAbort();
206
+ else signal?.addEventListener("abort", onAbort, { once: true });
207
+ });
208
+ }
209
+
210
+ export async function generateImage(config, {
211
+ prompt,
212
+ size = "1024x1024",
213
+ } = {}, {
214
+ fetchImpl = fetch,
215
+ downloadImage = downloadPublicImage,
216
+ signal,
217
+ timeoutMs = IMAGE_GENERATION_TIMEOUT_MS,
218
+ } = {}) {
219
+ const normalizedPrompt = checkedPrompt(prompt);
220
+ const normalizedSize = checkedSize(size);
221
+ const timeout = AbortSignal.timeout(timeoutMs);
222
+ const requestSignal = signal ? AbortSignal.any([signal, timeout]) : timeout;
223
+ let response;
224
+ try {
225
+ response = await fetchImpl(config.endpoint, {
226
+ method: "POST",
227
+ headers: {
228
+ Authorization: `Bearer ${config.api_key}`,
229
+ "Content-Type": "application/json",
230
+ Accept: "application/json",
231
+ },
232
+ body: JSON.stringify(generationBody(config, normalizedPrompt, normalizedSize)),
233
+ signal: requestSignal,
234
+ });
235
+ } catch (error) {
236
+ if (requestSignal.aborted) throw providerError("Image generation timed out or was cancelled", "ccdx_image_timeout");
237
+ throw providerError("Image API could not be reached");
238
+ }
239
+ const body = await boundedJson(response);
240
+ if (!response.ok) {
241
+ const detail = upstreamMessage(body);
242
+ throw providerError(`Image API failed with HTTP ${response.status}${detail ? `: ${detail}` : ""}`);
243
+ }
244
+ const source = imageSource(body);
245
+ if (!source) throw providerError("Image API response did not contain an image");
246
+ let bytes;
247
+ try {
248
+ bytes = source.type === "base64"
249
+ ? Buffer.from(source.value, "base64")
250
+ : await downloadImage(source.value, { signal: requestSignal, maxBytes: IMAGE_MAX_BYTES });
251
+ } catch (error) {
252
+ if (error?.code?.startsWith?.("ccdx_")) throw error;
253
+ const code = String(error?.cause?.code || error?.code || "").replace(/[^A-Z0-9_-]/gi, "").slice(0, 64);
254
+ throw providerError(`Generated image could not be downloaded${code ? ` (${code})` : ""}`);
255
+ }
256
+ if (!bytes.length || bytes.length > IMAGE_MAX_BYTES) {
257
+ throw providerError("Generated image is empty or exceeds the size limit", "ccdx_image_too_large");
258
+ }
259
+ const metadata = imageMetadata(bytes);
260
+ return {
261
+ data: bytes.toString("base64"),
262
+ mimeType: metadata.mimeType,
263
+ width: metadata.width,
264
+ height: metadata.height,
265
+ model: config.model,
266
+ size: normalizedSize,
267
+ };
268
+ }
@@ -154,6 +154,7 @@ export function runtimeStatusPayload({
154
154
  streamPerformance,
155
155
  admission,
156
156
  imagePressure,
157
+ responseFailures,
157
158
  modelRegistry,
158
159
  codexClient,
159
160
  codexModelRegistry,
@@ -177,6 +178,7 @@ export function runtimeStatusPayload({
177
178
  response_history: responseHistoryStats(),
178
179
  image_optimization: imageOptimizationStats(),
179
180
  image_history_pressure: imagePressure?.snapshot?.() || null,
181
+ response_failures: responseFailures?.snapshot?.() || null,
180
182
  copilot: codexRuntime,
181
183
  models: codexModels,
182
184
  profiles: {
@@ -0,0 +1,100 @@
1
+ import { terminalCell } from "./cli-table.mjs";
2
+
3
+ const DEFAULT_RECENT_FAILURES = 10;
4
+
5
+ function safeIdentifier(value, fallback = "unknown") {
6
+ const text = terminalCell(value, { fallback });
7
+ return text.slice(0, 200);
8
+ }
9
+
10
+ function safeFailureMessage(value) {
11
+ let text = terminalCell(value, { fallback: "Response failed without an error message" });
12
+ text = text
13
+ .replace(/(encrypted(?: function output)? content\s+)[A-Za-z0-9+/_=-]{24,}/gi, "$1[redacted]")
14
+ .replace(/\b[A-Za-z0-9+/_=-]{96,}\b/g, "[redacted]");
15
+ return text.slice(0, 500);
16
+ }
17
+
18
+ function upstreamRequestId(headers) {
19
+ if (!headers?.get) return null;
20
+ for (const name of ["x-request-id", "x-github-request-id", "x-ms-request-id", "request-id", "x-correlation-id"]) {
21
+ const value = headers.get(name);
22
+ if (value) return safeIdentifier(value, "");
23
+ }
24
+ return null;
25
+ }
26
+
27
+ export function responseFailureDetails(event, eventType = event?.type, {
28
+ model,
29
+ headers,
30
+ retried = false,
31
+ retryPolicy = null,
32
+ retrySkipped = null,
33
+ } = {}) {
34
+ const response = event?.response && typeof event.response === "object" ? event.response : event;
35
+ const error = response?.error && typeof response.error === "object"
36
+ ? response.error
37
+ : event?.error && typeof event.error === "object"
38
+ ? event.error
39
+ : event;
40
+ return {
41
+ at: new Date().toISOString(),
42
+ event_type: safeIdentifier(eventType || "response.failed"),
43
+ model: safeIdentifier(response?.model || model),
44
+ code: safeIdentifier(error?.code || "unknown_error"),
45
+ message: safeFailureMessage(error?.message),
46
+ response_id: response?.id ? safeIdentifier(response.id, "") : null,
47
+ upstream_request_id: upstreamRequestId(headers),
48
+ retried: Boolean(retried),
49
+ retry_policy: retryPolicy ? safeIdentifier(retryPolicy, "") : null,
50
+ retry_skipped: retrySkipped ? safeIdentifier(retrySkipped, "") : null,
51
+ };
52
+ }
53
+
54
+ export function responseFailureText(event, eventType = event?.type) {
55
+ const response = event?.response && typeof event.response === "object" ? event.response : event;
56
+ const error = response?.error && typeof response.error === "object"
57
+ ? response.error
58
+ : event?.error && typeof event.error === "object"
59
+ ? event.error
60
+ : event;
61
+ return JSON.stringify({ error: { code: error?.code, message: error?.message } });
62
+ }
63
+
64
+ export function formatResponseFailureLog(details) {
65
+ const fields = [
66
+ `Responses ${details.event_type}`,
67
+ `model=${details.model}`,
68
+ `code=${details.code}`,
69
+ details.response_id ? `response_id=${details.response_id}` : null,
70
+ details.upstream_request_id ? `upstream_request_id=${details.upstream_request_id}` : null,
71
+ details.retried ? `retry=${details.retry_policy || "yes"}` : null,
72
+ details.retry_skipped ? `retry_skipped=${details.retry_skipped}` : null,
73
+ `message=${details.message}`,
74
+ ].filter(Boolean);
75
+ return fields.join(" ");
76
+ }
77
+
78
+ export function createResponseFailureDiagnostics({
79
+ maxEntries = DEFAULT_RECENT_FAILURES,
80
+ now = () => new Date().toISOString(),
81
+ } = {}) {
82
+ const recent = [];
83
+ let total = 0;
84
+ let retried = 0;
85
+ return {
86
+ record(details) {
87
+ total += 1;
88
+ if (details.retried) retried += 1;
89
+ recent.push({ ...details, at: now() });
90
+ if (recent.length > maxEntries) recent.splice(0, recent.length - maxEntries);
91
+ },
92
+ snapshot() {
93
+ return {
94
+ total,
95
+ retried,
96
+ recent: recent.map((entry) => ({ ...entry })),
97
+ };
98
+ },
99
+ };
100
+ }
@@ -109,6 +109,7 @@ export function createResponsesHandler(options) {
109
109
  requestBodyTimeoutMs,
110
110
  responsesPayloadOptions,
111
111
  responsesFn,
112
+ responseFailures,
112
113
  streamHandshakeTimeoutMs,
113
114
  streamIdleTimeoutMs,
114
115
  upstreamTimeoutMs,
@@ -255,6 +256,7 @@ export function createResponsesHandler(options) {
255
256
  abort,
256
257
  onUpstreamStart: startUpstreamTimeout,
257
258
  releaseRequest: releaseUpstreamPayload,
259
+ responseFailures,
258
260
  streamIdleTimeoutMs,
259
261
  });
260
262
  if (result?.successful) imagePressure?.markSuccess?.(responseHistoryPressureRootId(prepared));
@@ -8,8 +8,18 @@ import {
8
8
  sendUpstreamError,
9
9
  writeOrDrain,
10
10
  } from "./http-transport.mjs";
11
- import { openCopilotResponse } from "./copilot-responses-compat.mjs";
12
- import { rememberResponseHistory } from "./responses-request.mjs";
11
+ import { openCopilotResponse, selectCopilotResponseRetry } from "./copilot-responses-compat.mjs";
12
+ import {
13
+ dropMaterializedResponseHistory,
14
+ hasEncryptedResponseState,
15
+ rememberResponseHistory,
16
+ restoreMaterializedResponseHistoryForRetry,
17
+ } from "./responses-request.mjs";
18
+ import {
19
+ formatResponseFailureLog,
20
+ responseFailureDetails,
21
+ responseFailureText,
22
+ } from "./response-failures.mjs";
13
23
  import {
14
24
  compactionInputWithoutTrigger,
15
25
  parseResponsesCompactionResult,
@@ -30,9 +40,11 @@ import {
30
40
  } from "./stream-performance.mjs";
31
41
  import { safeUpstreamResponseHeaders } from "./upstream-headers.mjs";
32
42
  import { recordResponsesUsage } from "./usage.mjs";
43
+ import { status } from "./status.mjs";
33
44
 
34
45
  const MAX_SSE_BUFFER_BYTES = loadRuntimeConfig().maxSseBufferBytes;
35
46
  const SSE_WRITE_BATCH_BYTES = 64 * 1024;
47
+ const STREAM_RETRY_PRELUDE_MAX_BYTES = 256 * 1024;
36
48
  const RESPONSES_TERMINAL_EVENT_TYPES = new Set([
37
49
  "response.completed",
38
50
  "response.incomplete",
@@ -124,7 +136,9 @@ function inspectResponseSseEvent(state, eventName, data) {
124
136
  );
125
137
  }
126
138
  state.sawTerminal = true;
127
- if (eventType === "response.failed" || eventType === "error") markStreamFailure();
139
+ if (eventType === "response.failed" || eventType === "error") {
140
+ state.failure = { event, eventType };
141
+ }
128
142
  if (eventType === "response.completed") {
129
143
  const response = event.response;
130
144
  state.completed = { response, event };
@@ -292,68 +306,182 @@ function storeCompletedResponse(reqContext, completed) {
292
306
  });
293
307
  }
294
308
 
295
- export async function proxyCopilotResponses(reqContext, req, res, upstream = copilotResponses, options = {}) {
296
- let opened;
309
+ function streamHeaders(resp) {
310
+ return {
311
+ ...safeUpstreamResponseHeaders(resp.headers, { defaultContentType: "text/event-stream" }),
312
+ "Cache-Control": "no-cache",
313
+ Connection: "keep-alive",
314
+ };
315
+ }
316
+
317
+ function selectStreamFailureRetry(reqContext, failure, usedRetryPolicies) {
318
+ if (!failure) return null;
297
319
  try {
298
- opened = await openCopilotResponse(reqContext, upstream, options);
299
- } finally {
300
- options.releaseRequest?.(opened?.reqContext);
301
- }
302
- const { resp, errorText } = opened;
303
- reqContext = opened.reqContext;
304
- if (errorText !== undefined) {
305
- sendUpstreamError(res, resp, errorText);
306
- return { successful: false, compacted: false, upstreamStatus: resp.status };
320
+ const restored = restoreMaterializedResponseHistoryForRetry(reqContext);
321
+ return {
322
+ retry: selectCopilotResponseRetry(
323
+ restored,
324
+ 400,
325
+ responseFailureText(failure.event, failure.eventType),
326
+ usedRetryPolicies,
327
+ ),
328
+ retrySkipped: null,
329
+ };
330
+ } catch {
331
+ return { retry: null, retrySkipped: "history_unavailable" };
307
332
  }
333
+ }
308
334
 
309
- if (reqContext.body.stream) {
335
+ function recordResponseFailure(options, reqContext, resp, failure, {
336
+ retry,
337
+ retrySkipped,
338
+ } = {}) {
339
+ if (!failure) return;
340
+ const details = responseFailureDetails(failure.event, failure.eventType, {
341
+ model: reqContext.body?.model,
342
+ headers: resp.headers,
343
+ retried: Boolean(retry),
344
+ retryPolicy: retry?.policyId,
345
+ retrySkipped,
346
+ });
347
+ options.responseFailures?.record?.(details);
348
+ console.warn(status("warn", formatResponseFailureLog(details)));
349
+ }
350
+
351
+ async function proxyStreamingResponses(opened, res, upstream, options) {
352
+ while (true) {
353
+ let {
354
+ resp,
355
+ errorText,
356
+ reqContext,
357
+ streamRetryEligible = false,
358
+ usedRetryPolicies = new Set(),
359
+ } = opened;
360
+ if (errorText !== undefined) {
361
+ sendUpstreamError(res, resp, errorText);
362
+ return { successful: false, compacted: false, upstreamStatus: resp.status };
363
+ }
310
364
  await requireUpstreamEventStream(resp);
311
365
  options.abort?.setTimeout(options.streamIdleTimeoutMs, "stream_idle_timeout");
312
- res.writeHead(resp.status, {
313
- ...safeUpstreamResponseHeaders(resp.headers, { defaultContentType: "text/event-stream" }),
314
- "Cache-Control": "no-cache",
315
- Connection: "keep-alive",
316
- });
366
+ const headers = streamHeaders(resp);
367
+ let headersSent = false;
368
+ const writeHeaders = () => {
369
+ if (headersSent || res.headersSent) return;
370
+ headersSent = true;
371
+ res.writeHead(resp.status, headers);
372
+ };
317
373
  const reader = resp.body.getReader();
318
374
  const streamState = {
319
375
  completed: null,
376
+ failure: null,
320
377
  sawTerminal: false,
321
378
  sawOutput: false,
322
379
  toolArgumentGuard: new ToolArgumentDeltaGuard(),
323
380
  messageIds: new Map(),
324
381
  };
325
382
  const transformer = createResponseSseTransformer(streamState);
383
+ let holdPrelude = streamRetryEligible;
384
+ let retryDisabledReason = holdPrelude ? null : "request_not_eligible";
385
+ const prelude = [];
386
+ let preludeBytes = 0;
387
+ const flushPrelude = async () => {
388
+ if (!preludeBytes) return true;
389
+ writeHeaders();
390
+ const body = prelude.length === 1 ? prelude[0] : Buffer.concat(prelude, preludeBytes);
391
+ prelude.length = 0;
392
+ preludeBytes = 0;
393
+ return writeOrDrain(res, body);
394
+ };
395
+ const forwardFrame = async (frame) => {
396
+ if (!frame) return true;
397
+ if (!holdPrelude) {
398
+ writeHeaders();
399
+ return writeOrDrain(res, frame);
400
+ }
401
+ prelude.push(frame);
402
+ preludeBytes += frame.byteLength;
403
+ if (!streamState.sawOutput && preludeBytes <= STREAM_RETRY_PRELUDE_MAX_BYTES) return true;
404
+ retryDisabledReason = streamState.sawOutput ? "output_started" : "prelude_limit";
405
+ holdPrelude = false;
406
+ return flushPrelude();
407
+ };
408
+
326
409
  try {
327
410
  while (true) {
328
411
  const { done, value } = await reader.read();
329
412
  if (done) {
330
413
  if (!streamState.sawTerminal) throw incompleteUpstreamStream("a terminal Responses event");
331
- storeCompletedResponse(reqContext, streamState.completed);
332
- res.end();
333
- return { successful: Boolean(streamState.completed), compacted: false };
414
+ break;
334
415
  }
335
416
  options.abort?.setTimeout(options.streamIdleTimeoutMs, "stream_idle_timeout");
336
- // Batch within this read, not across reads, and await drain before processing more.
337
417
  for (let offset = 0; offset < value.byteLength; offset += SSE_WRITE_BATCH_BYTES) {
338
418
  const frame = transformer.push(value.subarray(offset, offset + SSE_WRITE_BATCH_BYTES));
339
- if (frame && !await writeOrDrain(res, frame)) return;
419
+ if (!await forwardFrame(frame)) return { successful: false, compacted: false };
340
420
  if (streamState.sawTerminal) break;
341
421
  }
342
- if (streamState.sawTerminal) {
343
- storeCompletedResponse(reqContext, streamState.completed);
344
- res.end();
345
- return { successful: Boolean(streamState.completed), compacted: false };
346
- }
422
+ if (streamState.sawTerminal) break;
423
+ }
424
+
425
+ let retry = null;
426
+ if (streamState.failure && holdPrelude && !streamState.sawOutput) {
427
+ const selected = selectStreamFailureRetry(reqContext, streamState.failure, usedRetryPolicies);
428
+ retry = selected?.retry || null;
429
+ if (!retry) retryDisabledReason = selected?.retrySkipped || "not_applicable";
430
+ }
431
+ if (streamState.failure) {
432
+ recordResponseFailure(options, reqContext, resp, streamState.failure, {
433
+ retry,
434
+ retrySkipped: retry ? null : retryDisabledReason,
435
+ });
436
+ }
437
+ if (retry) {
438
+ console.warn(status("warn", retry.warning));
439
+ opened = await openCopilotResponse(retry.reqContext, upstream, {
440
+ ...options,
441
+ usedRetryPolicies: retry.usedPolicies,
442
+ });
443
+ opened.streamRetryEligible = hasEncryptedResponseState(opened.reqContext.body?.input);
444
+ dropMaterializedResponseHistory(opened.reqContext);
445
+ continue;
347
446
  }
348
- } catch (e) {
349
- logRequestFailure("Responses", e, options.abort);
350
- await endStreamWithError(res, e, options.abort);
447
+ if (streamState.failure) markStreamFailure();
448
+ if (holdPrelude && !await flushPrelude()) return { successful: false, compacted: false };
449
+ storeCompletedResponse(reqContext, streamState.completed);
450
+ if (!res.writableEnded) res.end();
451
+ return { successful: Boolean(streamState.completed), compacted: false };
452
+ } catch (error) {
453
+ logRequestFailure("Responses", error, options.abort);
454
+ writeHeaders();
455
+ await endStreamWithError(res, error, options.abort);
351
456
  return { successful: false, compacted: false };
352
457
  } finally {
353
458
  await reader.cancel().catch(() => {});
354
459
  reader.releaseLock();
355
460
  }
356
- } else {
461
+ }
462
+ }
463
+
464
+ export async function proxyCopilotResponses(reqContext, req, res, upstream = copilotResponses, options = {}) {
465
+ let opened;
466
+ try {
467
+ opened = await openCopilotResponse(reqContext, upstream, options);
468
+ opened.streamRetryEligible = opened.reqContext.body.stream === true
469
+ && hasEncryptedResponseState(opened.reqContext.body?.input);
470
+ } finally {
471
+ options.releaseRequest?.(opened?.reqContext);
472
+ }
473
+ reqContext = opened.reqContext;
474
+ if (reqContext.body.stream) {
475
+ return proxyStreamingResponses(opened, res, upstream, options);
476
+ }
477
+
478
+ while (true) {
479
+ const { resp, errorText, usedRetryPolicies = new Set() } = opened;
480
+ reqContext = opened.reqContext;
481
+ if (errorText !== undefined) {
482
+ sendUpstreamError(res, resp, errorText);
483
+ return { successful: false, compacted: false, upstreamStatus: resp.status };
484
+ }
357
485
  const data = resp.ok
358
486
  ? await readBoundedResponseText(resp, {
359
487
  maxBytes: MAX_UPSTREAM_RESPONSES_SUCCESS_BODY_BYTES,
@@ -385,6 +513,24 @@ export async function proxyCopilotResponses(reqContext, req, res, upstream = cop
385
513
  }
386
514
  if (resp.ok) {
387
515
  const response = parseSuccessfulResponsesResult(data);
516
+ if (response.status === "failed") {
517
+ const failure = { event: { type: "response.failed", response }, eventType: "response.failed" };
518
+ const selected = selectStreamFailureRetry(reqContext, failure, usedRetryPolicies);
519
+ const retry = selected?.retry || null;
520
+ recordResponseFailure(options, reqContext, resp, failure, {
521
+ retry,
522
+ retrySkipped: retry ? null : selected?.retrySkipped || "not_applicable",
523
+ });
524
+ if (retry) {
525
+ console.warn(status("warn", retry.warning));
526
+ opened = await openCopilotResponse(retry.reqContext, upstream, {
527
+ ...options,
528
+ usedRetryPolicies: retry.usedPolicies,
529
+ });
530
+ dropMaterializedResponseHistory(opened.reqContext);
531
+ continue;
532
+ }
533
+ }
388
534
  rememberResponseHistory(reqContext, response);
389
535
  recordResponsesUsage({
390
536
  surface: reqContext.surface,
@@ -397,7 +543,7 @@ export async function proxyCopilotResponses(reqContext, req, res, upstream = cop
397
543
  contentType: "application/json",
398
544
  }));
399
545
  res.end(data);
400
- return { successful: true, compacted: false };
546
+ return { successful: response.status !== "failed" && response.status !== "incomplete", compacted: false };
401
547
  }
402
548
  res.writeHead(resp.status, safeUpstreamResponseHeaders(resp.headers, {
403
549
  contentType: "application/json",