codex-copilot-dx 0.6.1 → 0.6.3

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,130 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import {
5
+ AUTH_PROFILE_CLAUDE,
6
+ readAuthProfileCredentials,
7
+ } from "./auth-profile.mjs";
8
+ import { PM_STUDIO_RECIPES } from "./pm-studio-asar.mjs";
9
+ import {
10
+ createPmStudioSetupOperations,
11
+ DEFAULT_PM_STUDIO_APP_PATH,
12
+ inspectPmStudioApp,
13
+ PM_STUDIO_CLAUDE_AUTH_COMMAND,
14
+ } from "./pm-studio-setup.mjs";
15
+ import { checkRunningAdapter } from "./running-adapter.mjs";
16
+ import { status } from "./status.mjs";
17
+
18
+ const PM_RELAY_HOST = "127.0.0.1";
19
+ const PM_RELAY_PORT = 2026;
20
+
21
+ function selectRecipe(metadata, recipes) {
22
+ return recipes.find((recipe) => recipe.version === String(metadata.version)
23
+ && recipe.build === String(metadata.build)
24
+ && recipe.bundleIdentifier === String(metadata.bundleIdentifier));
25
+ }
26
+
27
+ function safeMessage(error) {
28
+ return String(error?.message || "inspection failed").replace(/\s+/g, " ").trim();
29
+ }
30
+
31
+ function inspectClaudeProfile(home, readCredentials) {
32
+ try {
33
+ const credentials = readCredentials(AUTH_PROFILE_CLAUDE, { home });
34
+ return {
35
+ configured: credentials.configured === true,
36
+ valid: credentials.valid === true,
37
+ reason: credentials.reason || "",
38
+ login: credentials.identity?.login || "",
39
+ };
40
+ } catch (error) {
41
+ return { configured: true, valid: false, reason: safeMessage(error), login: "" };
42
+ }
43
+ }
44
+
45
+ export async function inspectPmStudioStatus({
46
+ appPath = DEFAULT_PM_STUDIO_APP_PATH,
47
+ home = os.homedir(),
48
+ recipes = PM_STUDIO_RECIPES,
49
+ operationOverrides = {},
50
+ existsSync = fs.existsSync,
51
+ inspectApp = inspectPmStudioApp,
52
+ readClaudeCredentials = readAuthProfileCredentials,
53
+ checkRunningAdapterFn = checkRunningAdapter,
54
+ } = {}) {
55
+ const operations = createPmStudioSetupOperations(operationOverrides);
56
+ let app;
57
+ if (!existsSync(appPath)) {
58
+ app = { state: "not_installed", metadata: null, issues: [] };
59
+ } else {
60
+ try {
61
+ const metadata = operations.readBundleMetadata({
62
+ appPath,
63
+ infoPlistPath: path.join(appPath, "Contents/Info.plist"),
64
+ processRunner: operations.processRunner,
65
+ });
66
+ const normalized = {
67
+ version: String(metadata.version),
68
+ build: String(metadata.build),
69
+ bundleIdentifier: String(metadata.bundleIdentifier),
70
+ };
71
+ const recipe = selectRecipe(normalized, recipes);
72
+ app = recipe
73
+ ? { ...inspectApp({ appPath, recipe, operations }), recipe: recipe.id }
74
+ : { state: "unsupported", metadata: normalized, issues: [], recipe: "" };
75
+ } catch (error) {
76
+ app = { state: "error", metadata: null, issues: [safeMessage(error)], recipe: "" };
77
+ }
78
+ }
79
+
80
+ const claude = inspectClaudeProfile(home, readClaudeCredentials);
81
+ let adapter;
82
+ try {
83
+ adapter = await checkRunningAdapterFn({ host: PM_RELAY_HOST, port: PM_RELAY_PORT });
84
+ } catch (error) {
85
+ adapter = { ok: false, error };
86
+ }
87
+ const operational = app.state === "patched" && claude.valid && adapter?.ok === true;
88
+ return { ok: operational, appPath, app, claude, adapter };
89
+ }
90
+
91
+ function appStatusLine(result, commandName) {
92
+ const { app } = result;
93
+ const version = app.metadata ? `${app.metadata.version} build ${app.metadata.build}` : "";
94
+ if (app.state === "patched") return status("ok", `PM Studio ${version} is patched and verified`);
95
+ if (app.state === "clean") return status("warn", `PM Studio ${version} is supported but not patched; run ${commandName} pms setup`);
96
+ if (app.state === "unsupported") return status("err", `PM Studio ${version} has no exact patch recipe; no files will be changed`);
97
+ if (app.state === "drift") return status("err", `PM Studio ${version} does not match the clean or patched recipe: ${app.issues.join("; ")}`);
98
+ if (app.state === "not_installed") return status("warn", `PM Studio is not installed at ${result.appPath}`);
99
+ return status("err", `PM Studio could not be inspected: ${app.issues.join("; ")}`);
100
+ }
101
+
102
+ export function formatPmStudioStatus(result, { commandName = "ccdx" } = {}) {
103
+ const lines = [
104
+ `${commandName} pms status`,
105
+ appStatusLine(result, commandName),
106
+ ];
107
+ if (result.claude.valid) {
108
+ const account = result.claude.login ? ` for ${result.claude.login}` : "";
109
+ lines.push(status("ok", `Isolated Claude profile is valid${account}`));
110
+ } else {
111
+ const reason = result.claude.configured && result.claude.reason ? ` (${result.claude.reason})` : "";
112
+ lines.push(status("err", `Isolated Claude profile is not ready${reason}; run ${PM_STUDIO_CLAUDE_AUTH_COMMAND}`));
113
+ }
114
+ if (result.adapter?.ok) {
115
+ lines.push(status("ok", `PM relay is available at ${result.adapter.baseUrl}`));
116
+ } else if (result.adapter?.incompatible) {
117
+ lines.push(status("err", `The adapter at ${result.adapter.baseUrl} is incompatible; stop it and run ${commandName} start`));
118
+ } else {
119
+ lines.push(status("warn", `PM relay is not running at http://${PM_RELAY_HOST}:${PM_RELAY_PORT}; run ${commandName} start`));
120
+ }
121
+ lines.push(status("info", "Routing: PM GPT uses the PM Studio bearer; eligible Claude chat uses the isolated Claude profile"));
122
+ return lines.join("\n");
123
+ }
124
+
125
+ export async function runPmStudioStatus(options = {}) {
126
+ const result = await inspectPmStudioStatus(options);
127
+ const output = formatPmStudioStatus(result, options);
128
+ (options.log || console.log)(output);
129
+ return { ...result, output };
130
+ }
@@ -33,6 +33,7 @@ export function createProfileModelRuntime({
33
33
  codexClient,
34
34
  claudeClient = codexClient,
35
35
  claudeMode = "inherited",
36
+ codexCredentialFingerprint = "",
36
37
  claudeCredentialFingerprint = "",
37
38
  home,
38
39
  env = process.env,
@@ -93,7 +94,7 @@ export function createProfileModelRuntime({
93
94
  function loadCached({ client, profile, registry, ownsClaudeModels }) {
94
95
  const credentialFingerprint = profile === "claude"
95
96
  ? String(claudeCredentialFingerprint || "").trim()
96
- : "";
97
+ : String(codexCredentialFingerprint || "").trim();
97
98
  if (profile === "claude" && !credentialFingerprint) return false;
98
99
  const cached = loadModelCache({ home, profile, credentialFingerprint });
99
100
  if (!cached) return false;
@@ -133,7 +134,7 @@ export function createProfileModelRuntime({
133
134
  function persist(models, profile) {
134
135
  const credentialFingerprint = profile === "claude"
135
136
  ? String(claudeCredentialFingerprint || "").trim()
136
- : "";
137
+ : String(codexCredentialFingerprint || "").trim();
137
138
  if (profile === "claude" && !credentialFingerprint) return;
138
139
  try {
139
140
  saveModelCache(models, { home, profile, credentialFingerprint });
@@ -0,0 +1,74 @@
1
+ function catalogData(catalog) {
2
+ if (Array.isArray(catalog)) return catalog;
3
+ return Array.isArray(catalog?.data) ? catalog.data : [];
4
+ }
5
+
6
+ function isClaudeCatalogEntry(model) {
7
+ const id = String(model?.id || "").trim();
8
+ const vendor = String(model?.vendor || "").trim().toLowerCase();
9
+ return Boolean(id) && (vendor === "anthropic" || id.toLowerCase().startsWith("claude-"));
10
+ }
11
+
12
+ function isAllowedClaudeModel(model) {
13
+ return isClaudeCatalogEntry(model)
14
+ && String(model?.vendor || "").trim().toLowerCase() === "anthropic"
15
+ && model?.model_picker_enabled !== false
16
+ && Array.isArray(model?.supported_endpoints)
17
+ && model.supported_endpoints.includes("/chat/completions");
18
+ }
19
+
20
+ export function profileRouting({ claudeMode = "inherited", claudeConfigured = false } = {}) {
21
+ const isolated = claudeMode === "isolated" || claudeConfigured === true;
22
+ return Object.freeze({ responses: "codex", messages: isolated ? "claude" : "codex" });
23
+ }
24
+
25
+ export function createPmStudioModelRouter({ getCatalog, isClaudeEnabled } = {}) {
26
+ if (typeof getCatalog !== "function") throw new Error("getCatalog is required");
27
+ if (typeof isClaudeEnabled !== "function") throw new Error("isClaudeEnabled is required");
28
+
29
+ const unset = Symbol("unset");
30
+ let cachedCatalog = unset;
31
+ let cachedEnabled = false;
32
+ let cachedAllowed = new Map();
33
+ let cachedKnown = new Set();
34
+ let rebuilds = 0;
35
+
36
+ function availability() {
37
+ const catalog = getCatalog();
38
+ const enabled = isClaudeEnabled() === true;
39
+ if (catalog === cachedCatalog && enabled === cachedEnabled) {
40
+ return { allowed: cachedAllowed, known: cachedKnown };
41
+ }
42
+
43
+ const allowed = new Map();
44
+ const known = new Set();
45
+ for (const model of catalogData(catalog)) {
46
+ const id = String(model?.id || "").trim();
47
+ if (!id || !isClaudeCatalogEntry(model)) continue;
48
+ known.add(id);
49
+ if (enabled && isAllowedClaudeModel(model) && !allowed.has(id)) allowed.set(id, model);
50
+ }
51
+ cachedCatalog = catalog;
52
+ cachedEnabled = enabled;
53
+ cachedAllowed = allowed;
54
+ cachedKnown = known;
55
+ rebuilds += 1;
56
+ return { allowed, known };
57
+ }
58
+
59
+ return Object.freeze({
60
+ classify(modelId) {
61
+ const id = String(modelId || "").trim();
62
+ const { allowed, known } = availability();
63
+ if (allowed.has(id)) return "claude";
64
+ if (known.has(id) || id.toLowerCase().startsWith("claude-")) return "unsupported_claude";
65
+ return "enterprise";
66
+ },
67
+ allowedModels() {
68
+ return [...availability().allowed.values()];
69
+ },
70
+ diagnostics() {
71
+ return { rebuilds, allowed: cachedAllowed.size, known: cachedKnown.size };
72
+ },
73
+ });
74
+ }
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  AUTH_PROFILE_CLAUDE,
3
+ AUTH_PROFILE_CODEX,
3
4
  authProfilePaths,
4
5
  profileReauthMessage,
5
6
  readAuthProfileCredentials,
@@ -45,6 +46,16 @@ export function createProfileRuntime({
45
46
  };
46
47
  const credentials = readCredentials();
47
48
  const claudeProfile = safeProfileStatus(credentials);
49
+ let codexCredentialFingerprint = "";
50
+ try {
51
+ const codexCredentials = readAuthProfileCredentials(AUTH_PROFILE_CODEX, { home });
52
+ if (codexCredentials.valid) {
53
+ codexCredentialFingerprint = githubTokenFingerprint(codexCredentials.token);
54
+ }
55
+ } catch {
56
+ // The Codex client remains authoritative if its optional metadata cannot
57
+ // be inspected; only credential-bound cache reuse is disabled.
58
+ }
48
59
 
49
60
  if (!credentials.configured) {
50
61
  return Object.freeze({
@@ -52,6 +63,7 @@ export function createProfileRuntime({
52
63
  claudeClient: codexClient,
53
64
  claudeMode: "inherited",
54
65
  claudeProfile,
66
+ codexCredentialFingerprint,
55
67
  claudeCredentialFingerprint: "",
56
68
  });
57
69
  }
@@ -73,6 +85,7 @@ export function createProfileRuntime({
73
85
  claudeClient,
74
86
  claudeMode: "isolated",
75
87
  claudeProfile,
88
+ codexCredentialFingerprint,
76
89
  claudeCredentialFingerprint: credentials.valid
77
90
  ? githubTokenFingerprint(credentials.token)
78
91
  : "",
@@ -174,6 +174,10 @@ export function responseHistoryStats() {
174
174
  return { entries: histories.size, bytes: totalBytes, evicted: evictedIds.size };
175
175
  }
176
176
 
177
+ export function responseHistoryRootId(responseId) {
178
+ return histories.get(responseId)?.rootId || null;
179
+ }
180
+
177
181
  export function materializeResponseHistory(responseId) {
178
182
  const chain = [];
179
183
  const seen = new Set();
@@ -149,24 +149,29 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
149
149
  try {
150
150
  const chatCompletionsFn = options.chatCompletionsFn || chatCompletions;
151
151
  try {
152
- resp = await chatCompletionsFn(upstreamReq, { signal: options.signal, bodyText });
152
+ options.onUpstreamStart?.();
153
+ resp = await chatCompletionsFn(upstreamReq, {
154
+ signal: options.signal,
155
+ bodyText,
156
+ onUpstreamStart: options.onUpstreamStart,
157
+ });
153
158
  } finally {
154
159
  options.releaseRequest?.();
155
160
  }
156
161
  } catch (e) {
157
162
  const statusCode = isAbortLikeError(e) ? abortErrorStatusCode(options.abort?.reason) : 502;
158
163
  await onError(statusCode, e.message);
159
- return;
164
+ return false;
160
165
  }
161
166
  if (!resp.ok) {
162
167
  await onError(resp.status, await resp.text(), undefined, resp);
163
- return;
168
+ return false;
164
169
  }
165
170
  try {
166
171
  await requireUpstreamEventStream(resp);
167
172
  } catch (error) {
168
173
  await onError(error.statusCode, error.message, error);
169
- return;
174
+ return false;
170
175
  }
171
176
  options.onUpstreamResponse?.(resp);
172
177
  options.abort?.setTimeout(options.streamIdleTimeoutMs, "stream_idle_timeout");
@@ -279,7 +284,7 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
279
284
  })) {
280
285
  if (!line.startsWith("data: ")) continue;
281
286
  const data = line.slice(6).trim();
282
- if (data === "[DONE]") { await emitCompleted(); onDone(); return; }
287
+ if (data === "[DONE]") { await emitCompleted(); onDone(); return true; }
283
288
  let parsed;
284
289
  try {
285
290
  parsed = JSON.parse(data);
@@ -331,8 +336,9 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
331
336
  } catch (e) {
332
337
  const statusCode = isAbortLikeError(e) ? abortErrorStatusCode(options.abort?.reason) : (e?.statusCode || 502);
333
338
  await onError(statusCode, e?.message || "upstream stream error", e);
334
- return;
339
+ return false;
335
340
  }
336
341
  const error = incompleteUpstreamStream("[DONE]");
337
342
  await onError(error.statusCode, error.message, error);
343
+ return false;
338
344
  }
@@ -159,12 +159,14 @@ function recordBytesAfterDrop(record, state, partIndex) {
159
159
  return state.baseBytes + remainingElementBytes + remainingCount - 1;
160
160
  }
161
161
 
162
- function selectImageDrops(refs, bodyBytes, targetBytes) {
162
+ function selectImageDrops(refs, bodyBytes, targetBytes, maxHistoricalImages = Number.POSITIVE_INFINITY) {
163
163
  const candidates = orderedHistoricalImageCandidates(refs);
164
164
  const selected = new Set();
165
165
  const byRecord = new Map();
166
166
  let projectedBytes = bodyBytes;
167
+ let historicalImages = refs.reduce((count, ref) => count + (ref.historical ? 1 : 0), 0);
167
168
  for (const index of candidates) {
169
+ if (projectedBytes <= targetBytes && historicalImages <= maxHistoricalImages) break;
168
170
  const ref = refs[index];
169
171
  let state = byRecord.get(ref.record);
170
172
  if (!state) {
@@ -173,7 +175,7 @@ function selectImageDrops(refs, bodyBytes, targetBytes) {
173
175
  }
174
176
  const afterBytes = recordBytesAfterDrop(ref.record, state, ref.partIndex);
175
177
  const savings = state.currentBytes - afterBytes;
176
- if (savings <= 0) continue;
178
+ if (savings <= 0 && historicalImages <= maxHistoricalImages) continue;
177
179
  state.currentBytes = afterBytes;
178
180
  if (ref.record.kind !== "top-level") {
179
181
  state.remainingCount -= 1;
@@ -181,9 +183,9 @@ function selectImageDrops(refs, bodyBytes, targetBytes) {
181
183
  }
182
184
  selected.add(index);
183
185
  projectedBytes -= savings;
184
- if (projectedBytes <= targetBytes) break;
186
+ historicalImages -= 1;
185
187
  }
186
- return selected;
188
+ return { selected, historicalImages, projectedBytes };
187
189
  }
188
190
 
189
191
  function applyImageDrops(records, refs, selected) {
@@ -253,7 +255,7 @@ export function trimResponsesHistoryToByteBudget(reqBody, {
253
255
  while (bodyBytes > limit) {
254
256
  const { records, refs } = collectResponseImages(inputItems, currentInputStart);
255
257
  if (!refs.length) break;
256
- const selected = selectImageDrops(refs, bodyBytes, limit);
258
+ const { selected } = selectImageDrops(refs, bodyBytes, limit);
257
259
  if (!selected.size) break;
258
260
  applyImageDrops(records, refs, selected);
259
261
  imagesOmitted += selected.size;
@@ -278,6 +280,62 @@ export function trimResponsesHistoryToByteBudget(reqBody, {
278
280
  };
279
281
  }
280
282
 
283
+ export function responsesHistoricalImageStats(inputItems, currentInputStart = 0) {
284
+ const items = Array.isArray(inputItems) ? inputItems : [];
285
+ const { refs } = collectResponseImages(items, currentInputStart);
286
+ const historicalImages = refs.reduce((count, ref) => count + (ref.historical ? 1 : 0), 0);
287
+ return {
288
+ totalImages: refs.length,
289
+ historicalImages,
290
+ currentImages: refs.length - historicalImages,
291
+ };
292
+ }
293
+
294
+ export function trimResponsesHistoricalImages(reqBody, {
295
+ currentInputStart = 0,
296
+ maxHistoricalImages = Number.POSITIVE_INFINITY,
297
+ targetBytes = Number.POSITIVE_INFINITY,
298
+ initialBodyText,
299
+ initialBodyBytes,
300
+ } = {}) {
301
+ let bodyText = initialBodyText ?? JSON.stringify(reqBody);
302
+ let bodyBytes = initialBodyBytes ?? Buffer.byteLength(bodyText);
303
+ const byteLimit = Number.isFinite(targetBytes) && targetBytes > 0
304
+ ? Math.floor(targetBytes)
305
+ : Number.POSITIVE_INFINITY;
306
+ const imageLimit = Number.isFinite(maxHistoricalImages) && maxHistoricalImages >= 0
307
+ ? Math.floor(maxHistoricalImages)
308
+ : Number.POSITIVE_INFINITY;
309
+ const inputItems = Array.isArray(reqBody?.input) ? reqBody.input : [];
310
+ let imagesOmitted = 0;
311
+
312
+ while (true) {
313
+ const { records, refs } = collectResponseImages(inputItems, currentInputStart);
314
+ const historicalImages = refs.reduce((count, ref) => count + (ref.historical ? 1 : 0), 0);
315
+ if (bodyBytes <= byteLimit && historicalImages <= imageLimit) break;
316
+ if (!historicalImages) break;
317
+ const { selected } = selectImageDrops(refs, bodyBytes, byteLimit, imageLimit);
318
+ if (!selected.size) break;
319
+ applyImageDrops(records, refs, selected);
320
+ imagesOmitted += selected.size;
321
+ ({ bodyText, bodyBytes } = bodyPayload(reqBody));
322
+ }
323
+
324
+ const stats = responsesHistoricalImageStats(inputItems, currentInputStart);
325
+ return {
326
+ bodyText,
327
+ bodyBytes,
328
+ targetBytes: byteLimit,
329
+ maxHistoricalImages: imageLimit,
330
+ historicalImages: stats.historicalImages,
331
+ currentImages: stats.currentImages,
332
+ imagesOmitted,
333
+ overBudget: bodyBytes > byteLimit,
334
+ overImageBudget: stats.historicalImages > imageLimit,
335
+ adapted: imagesOmitted > 0,
336
+ };
337
+ }
338
+
281
339
  export function enforceResponsesPayloadByteBudget(reqBody, payload) {
282
340
  let finalized = payload;
283
341
  if (payload?.overBudget) {
@@ -79,6 +79,7 @@ export function createResponsesHandler(options) {
79
79
  autoReviewModelResolver,
80
80
  chatCompletionsFn,
81
81
  getCachedModelEndpointsFn = getCachedModelEndpoints,
82
+ imagePressure,
82
83
  openAIModelEnv,
83
84
  responsesPayloadOptions,
84
85
  responsesFn,
@@ -90,14 +91,42 @@ export function createResponsesHandler(options) {
90
91
  return async function handleResponses(req, res) {
91
92
  const abort = createRequestAbort(req, res);
92
93
  let releaseRequest = () => {};
94
+ let prepared = null;
95
+ let imagePressureResult = null;
96
+ let parsed = null;
97
+ const startUpstreamTimeout = () => abort.setTimeout(
98
+ parsed?.stream ? streamHandshakeTimeoutMs : upstreamTimeoutMs,
99
+ parsed?.stream ? "stream_handshake_timeout" : "upstream_timeout",
100
+ );
101
+ const applyAdaptiveTimeout = (error) => {
102
+ if (!["responses_prepare_timeout", "stream_handshake_timeout", "upstream_timeout"].includes(abort.reason)) {
103
+ return error;
104
+ }
105
+ const activated = imagePressure?.markTimeout?.(prepared?.historyRootId, {
106
+ eligible: imagePressureResult?.pressureEligible === true,
107
+ });
108
+ if (!activated) return error;
109
+ const message = "Responses timed out while processing a large visual history. Retry the request; ccdx will keep fewer earlier images automatically. Restart is not required.";
110
+ error.statusCode = 504;
111
+ error.jsonBody = {
112
+ error: {
113
+ message,
114
+ type: "timeout_error",
115
+ code: "ccdx_visual_history_timeout",
116
+ retryable: true,
117
+ },
118
+ };
119
+ return error;
120
+ };
93
121
  try {
94
122
  releaseRequest = await acquireRequest(req, { signal: abort.signal });
95
- const parsed = await readJsonBody(req);
96
- abort.setTimeout(
97
- parsed.stream ? streamHandshakeTimeoutMs : upstreamTimeoutMs,
98
- parsed.stream ? "stream_handshake_timeout" : "upstream_timeout",
99
- );
100
- const prepared = prepareResponsesRequest(parsed, { mutate: true });
123
+ parsed = await readJsonBody(req);
124
+ abort.setTimeout(upstreamTimeoutMs, "responses_prepare_timeout");
125
+ prepared = prepareResponsesRequest(parsed, { mutate: true });
126
+ imagePressureResult = imagePressure?.apply?.(prepared) || null;
127
+ if (imagePressureResult?.adapted) {
128
+ console.warn(status("warn", `responses visual history mode=${imagePressureResult.mode} historical_images=${imagePressureResult.initialHistoricalImages}->${imagePressureResult.historicalImages} omitted=${imagePressureResult.imagesOmitted} bytes=${imagePressureResult.initialBodyBytes}->${imagePressureResult.bodyBytes}`));
129
+ }
101
130
  prepared.surface = "responses";
102
131
  const model = parsed.model || "unknown";
103
132
  const { requestedModel, upstreamModel } = resolveRequestModel(model, openAIModelEnv, autoReviewModelResolver);
@@ -110,12 +139,14 @@ export function createResponsesHandler(options) {
110
139
  || (usesCustomTools && cachedModelSupportsResponses(upstreamModel, getCachedModelEndpointsFn));
111
140
  if (usesCustomTools && !useNativeResponses) throw unsupportedCustomToolsError(upstreamModel);
112
141
  if (useNativeResponses) {
113
- await proxyCopilotResponses(prepared, req, res, responsesFn, {
142
+ const result = await proxyCopilotResponses(prepared, req, res, responsesFn, {
114
143
  signal: abort.signal,
115
144
  abort,
145
+ onUpstreamStart: startUpstreamTimeout,
116
146
  releaseRequest,
117
147
  streamIdleTimeoutMs,
118
148
  });
149
+ if (result?.successful) imagePressure?.markSuccess?.(prepared.historyRootId);
119
150
  } else {
120
151
  const chatPayload = await prepareResponsesChatPayload(prepared, {
121
152
  payloadOptions: responsesPayloadOptions,
@@ -125,7 +156,7 @@ export function createResponsesHandler(options) {
125
156
  const { chatReq, bodyText } = chatPayload;
126
157
  if (parsed.stream) {
127
158
  let streamResponseHeaders = null;
128
- await forwardToChat(chatReq, async (event, data) => {
159
+ const successful = await forwardToChat(chatReq, async (event, data) => {
129
160
  if (!res.headersSent) res.writeHead(200, {
130
161
  ...streamResponseHeaders,
131
162
  "Cache-Control": "no-cache",
@@ -142,7 +173,7 @@ export function createResponsesHandler(options) {
142
173
  sendUpstreamError(res, upstreamResponse, errMsg);
143
174
  return;
144
175
  }
145
- const responseError = error || Object.assign(new Error(errMsg), { statusCode });
176
+ const responseError = applyAdaptiveTimeout(error || Object.assign(new Error(errMsg), { statusCode }));
146
177
  sendJsonError(res, responseError, statusCode || 500);
147
178
  return;
148
179
  }
@@ -150,6 +181,7 @@ export function createResponsesHandler(options) {
150
181
  }, {
151
182
  signal: abort.signal,
152
183
  abort,
184
+ onUpstreamStart: startUpstreamTimeout,
153
185
  streamIdleTimeoutMs,
154
186
  chatCompletionsFn,
155
187
  releaseRequest,
@@ -160,9 +192,15 @@ export function createResponsesHandler(options) {
160
192
  });
161
193
  },
162
194
  });
195
+ if (successful) imagePressure?.markSuccess?.(prepared.historyRootId);
163
196
  } else {
164
197
  try {
165
- const upstream = await chatCompletionsFn(chatReq, { signal: abort.signal, bodyText });
198
+ startUpstreamTimeout();
199
+ const upstream = await chatCompletionsFn(chatReq, {
200
+ signal: abort.signal,
201
+ bodyText,
202
+ onUpstreamStart: startUpstreamTimeout,
203
+ });
166
204
  releaseRequest();
167
205
  const data = await upstream.text();
168
206
  if (!upstream.ok) {
@@ -171,20 +209,23 @@ export function createResponsesHandler(options) {
171
209
  }
172
210
  const response = chatToResponses(JSON.parse(data), model);
173
211
  rememberResponseHistory(prepared, response);
212
+ imagePressure?.markSuccess?.(prepared.historyRootId);
174
213
  recordResponsesUsage({ surface: prepared.surface, mode: "json", model, response, event: response });
175
214
  res.writeHead(200, safeUpstreamResponseHeaders(upstream.headers, {
176
215
  contentType: "application/json",
177
216
  }));
178
217
  res.end(JSON.stringify(response));
179
218
  } catch (error) {
180
- logRequestFailure("Responses", error, abort);
181
- sendJsonError(res, error, 502);
219
+ const responseError = applyAdaptiveTimeout(error);
220
+ logRequestFailure("Responses", responseError, abort);
221
+ sendJsonError(res, responseError, 502);
182
222
  }
183
223
  }
184
224
  }
185
225
  } catch (error) {
186
- logRequestFailure("Responses", error, abort);
187
- sendJsonError(res, error, 502);
226
+ const responseError = applyAdaptiveTimeout(error);
227
+ logRequestFailure("Responses", responseError, abort);
228
+ sendJsonError(res, responseError, 502);
188
229
  } finally {
189
230
  releaseRequest();
190
231
  abort.cleanup();
@@ -196,6 +237,7 @@ export function createResponsesCompactHandler(options) {
196
237
  const {
197
238
  acquireRequest,
198
239
  autoReviewModelResolver,
240
+ imagePressure,
199
241
  openAIModelEnv,
200
242
  responsesCompactFn,
201
243
  upstreamTimeoutMs,
@@ -207,7 +249,7 @@ export function createResponsesCompactHandler(options) {
207
249
  try {
208
250
  releaseRequest = await acquireRequest(req, { signal: abort.signal });
209
251
  const parsed = await readJsonBody(req);
210
- abort.setTimeout(upstreamTimeoutMs, "upstream_timeout");
252
+ abort.setTimeout(upstreamTimeoutMs, "responses_prepare_timeout");
211
253
  const prepared = prepareResponsesCompactionRequest(
212
254
  prepareResponsesRequest(parsed, { mutate: true }),
213
255
  );
@@ -217,11 +259,13 @@ export function createResponsesCompactHandler(options) {
217
259
  if (upstreamModel !== requestedModel) prepared.body.model = upstreamModel;
218
260
  const upstreamLog = upstreamModel === requestedModel ? "" : ` upstream_model=${upstreamModel}`;
219
261
  console.log(status("info", `responses compact model=${requestedModel}${upstreamLog} stream=false`));
220
- await proxyCopilotResponses(prepared, req, res, responsesCompactFn, {
262
+ const result = await proxyCopilotResponses(prepared, req, res, responsesCompactFn, {
221
263
  signal: abort.signal,
222
264
  abort,
265
+ onUpstreamStart: () => abort.setTimeout(upstreamTimeoutMs, "upstream_timeout"),
223
266
  releaseRequest,
224
267
  });
268
+ if (result?.compacted) imagePressure?.clear?.(prepared.historyRootId);
225
269
  } catch (error) {
226
270
  logRequestFailure("Responses compact", error, abort);
227
271
  sendJsonError(res, error, 502);