codex-overleaf-link 2.1.5 → 2.2.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.
Files changed (67) hide show
  1. package/README.md +27 -23
  2. package/extension/bootstrap/background.js +14 -1
  3. package/extension/bootstrap/manifest.template.json +1 -1
  4. package/extension/runtime-manifest.json +10 -0
  5. package/extension/src/background.js +507 -8
  6. package/extension/src/backgroundUpdateCoordinator.js +284 -19
  7. package/extension/src/content/activeTurnControl.js +179 -0
  8. package/extension/src/content/composerPanel.js +14 -5
  9. package/extension/src/content/contentRuntime.js +330 -309
  10. package/extension/src/content/diagnosticsPanel.js +132 -1
  11. package/extension/src/content/markdownDomRenderer.js +451 -0
  12. package/extension/src/content/markdownText.js +86 -7
  13. package/extension/src/content/mathText.js +49 -4
  14. package/extension/src/content/modelPicker.js +14 -5
  15. package/extension/src/content/nativeChannel.js +7 -1
  16. package/extension/src/content/panelRenderer.js +112 -12
  17. package/extension/src/content/pendingInputView.js +106 -0
  18. package/extension/src/content/projectProviderSelection.js +68 -0
  19. package/extension/src/content/projectSessionCleanup.js +4 -0
  20. package/extension/src/content/providerSettingsCoordinator.js +170 -22
  21. package/extension/src/content/providerSettingsDialog.js +2 -2
  22. package/extension/src/content/recentProjects.js +7 -2
  23. package/extension/src/content/runGuidanceController.js +70 -0
  24. package/extension/src/content/runGuidanceView.js +48 -0
  25. package/extension/src/content/runQueueScheduler.js +260 -0
  26. package/extension/src/content/runTimelineView.js +33 -80
  27. package/extension/src/content/sessionManager.js +2 -2
  28. package/extension/src/content/sessionPersistence.js +62 -0
  29. package/extension/src/content/settingsPanel.js +1 -1
  30. package/extension/src/content/updateNotice.js +80 -1
  31. package/extension/src/page/writebackRouter.js +3 -9
  32. package/extension/src/shared/compatibility.js +2 -1
  33. package/extension/src/shared/i18n.js +38 -0
  34. package/extension/src/shared/providerProfiles.js +3 -2
  35. package/extension/src/shared/runInputQueue.js +253 -0
  36. package/extension/src/shared/sessionState.js +45 -22
  37. package/extension/src/shared/storageDb.js +39 -44
  38. package/extension/src/shared/storageMigration.js +123 -3
  39. package/extension/src/shared/storageRunActions.js +34 -1
  40. package/extension/src/shared/updateConsent.js +15 -4
  41. package/extension/styles/panel.css +319 -20
  42. package/extension/vendor/katex/katex.min.css +1 -1
  43. package/extension/vendor/markdown-it/LICENSE +22 -0
  44. package/extension/vendor/markdown-it/SOURCE.json +7 -0
  45. package/extension/vendor/markdown-it/VERSION +1 -0
  46. package/extension/vendor/markdown-it/markdown-it.min.js +3 -0
  47. package/native-host/src/anthropicBridgeRequest.js +3 -3
  48. package/native-host/src/anthropicBridgeResponse.js +58 -17
  49. package/native-host/src/anthropicMessagesBridge.js +156 -23
  50. package/native-host/src/chatBridgeRequest.js +1 -1
  51. package/native-host/src/chatBridgeResponse.js +12 -8
  52. package/native-host/src/chatCompletionsBridge.js +22 -40
  53. package/native-host/src/codexPromptAssembly.js +3 -1
  54. package/native-host/src/codexProviderLaunch.js +6 -3
  55. package/native-host/src/codexSessionRunner.js +65 -65
  56. package/native-host/src/codexSessionTiming.js +45 -0
  57. package/native-host/src/index.js +58 -3
  58. package/native-host/src/managedInstall.js +45 -7
  59. package/native-host/src/mirrorWorkspace.js +15 -8
  60. package/native-host/src/providerProfile.js +2 -2
  61. package/native-host/src/providerStreamLifecycle.js +100 -0
  62. package/native-host/src/responsesPassthroughBridge.js +636 -0
  63. package/native-host/src/taskRunnerRuntime.js +81 -2
  64. package/native-host/src/updateArchive.js +4 -3
  65. package/native-host/src/updateManager.js +306 -19
  66. package/native-host/src/updateTrust.js +1 -1
  67. package/package.json +3 -1
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const crypto = require('node:crypto');
4
+ const MAX_STREAM_BUFFER_BYTES = 8 * 1024 * 1024;
4
5
  const { encodeAnthropicThinkingBlock } = require('./anthropicBridgeRequest');
5
6
 
6
7
  function convertAnthropicResponse(message = {}, context = {}) {
@@ -44,30 +45,58 @@ function convertAnthropicResponse(message = {}, context = {}) {
44
45
  };
45
46
  }
46
47
 
47
- async function streamAnthropicResponse({ upstream, res, context = {}, onComplete = () => {} } = {}) {
48
+ async function streamAnthropicResponse({
49
+ upstream,
50
+ res,
51
+ context = {},
52
+ onComplete = () => {},
53
+ onActivity = () => {},
54
+ recoverInterrupted = () => ''
55
+ } = {}) {
48
56
  const state = createState(context);
49
57
  beginSse(res);
50
58
  emit(state, res, 'response.created', { response: streamEnvelope(state, 'in_progress') });
51
59
  emit(state, res, 'response.in_progress', { response: streamEnvelope(state, 'in_progress') });
52
60
  const decoder = new TextDecoder();
53
61
  let buffer = '';
54
- for await (const chunk of upstream.body || []) {
55
- buffer += typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true });
56
- let boundary = findBoundary(buffer);
57
- while (boundary) {
58
- const block = buffer.slice(0, boundary.index);
59
- buffer = buffer.slice(boundary.index + boundary.length);
60
- const event = parseEvent(block);
61
- if (event.data) processEvent(state, res, event.type, parseJson(event.data));
62
- boundary = findBoundary(buffer);
62
+ let done = false;
63
+ let interruptedReason = '';
64
+ try {
65
+ for await (const chunk of upstream.body || []) {
66
+ onActivity();
67
+ buffer += typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true });
68
+ if (Buffer.byteLength(buffer, 'utf8') > MAX_STREAM_BUFFER_BYTES) {
69
+ throw responseError('Provider stream frame exceeded the local bridge limit.', 'provider_stream_frame_too_large');
70
+ }
71
+ let boundary = findBoundary(buffer);
72
+ while (boundary) {
73
+ const block = buffer.slice(0, boundary.index);
74
+ buffer = buffer.slice(boundary.index + boundary.length);
75
+ const event = parseEvent(block);
76
+ if (event.data) processEvent(state, res, event.type, parseJson(event.data));
77
+ if (state.sawMessageStop || state.stopReason) {
78
+ done = true;
79
+ break;
80
+ }
81
+ boundary = findBoundary(buffer);
82
+ }
83
+ if (done) break;
63
84
  }
85
+ } catch (error) {
86
+ interruptedReason = String(recoverInterrupted(error) || '');
87
+ if (!interruptedReason || !hasRecoverablePartialOutput(state)) throw error;
64
88
  }
89
+ if (!done && !interruptedReason && hasRecoverablePartialOutput(state)) {
90
+ interruptedReason = 'stream_ended';
91
+ }
92
+ if (!done && !interruptedReason) {
65
93
  buffer += decoder.decode();
66
- if (buffer.trim()) {
67
- const event = parseEvent(buffer);
68
- if (event.data) processEvent(state, res, event.type, parseJson(event.data));
94
+ if (buffer.trim()) {
95
+ const event = parseEvent(buffer);
96
+ if (event.data) processEvent(state, res, event.type, parseJson(event.data));
97
+ }
69
98
  }
70
- const converted = completeStream(state, res);
99
+ const converted = completeStream(state, res, interruptedReason);
71
100
  onComplete(converted);
72
101
  res.end();
73
102
  return converted;
@@ -221,10 +250,12 @@ function closeBlock(state, res, index) {
221
250
  state.items[entry.outputIndex] = entry.item;
222
251
  }
223
252
 
224
- function completeStream(state, res) {
253
+ function completeStream(state, res, interruptedReason = '') {
225
254
  for (const index of Array.from(state.blocks.keys()).sort((a, b) => a - b)) closeBlock(state, res, index);
226
255
  if (!state.items.filter(Boolean).length) throw responseError('Anthropic stream ended without usable output.');
227
- const terminal = mapStopReason(state.stopReason);
256
+ const terminal = interruptedReason
257
+ ? { status: 'incomplete', incompleteReason: interruptedReason }
258
+ : mapStopReason(state.stopReason);
228
259
  const response = responseEnvelope({
229
260
  id: state.id,
230
261
  model: state.model,
@@ -237,6 +268,15 @@ function completeStream(state, res) {
237
268
  return { response, assistantBlocks: state.assistantBlocks.filter(Boolean) };
238
269
  }
239
270
 
271
+ function hasRecoverablePartialOutput(state) {
272
+ const entries = Array.from(state.blocks.values());
273
+ if (entries.some(entry => entry?.type === 'tool_use')) return false;
274
+ return entries.some(entry => (
275
+ (entry?.type === 'text' && entry.text)
276
+ || ((entry?.type === 'thinking' || entry?.type === 'redacted_thinking') && (entry.thinking || entry.data))
277
+ ));
278
+ }
279
+
240
280
  function toolItem(block, toolContext) {
241
281
  const spec = resolveToolSpec(toolContext, block.name);
242
282
  const kind = spec?.kind === 'custom' ? 'custom' : 'function';
@@ -289,7 +329,8 @@ function responseEnvelope({ id, model, output, usage, requestBody = {}, status,
289
329
  }
290
330
 
291
331
  function mapStopReason(value) {
292
- if (value === 'max_tokens' || value === 'model_context_window_exceeded') return { status: 'incomplete', incompleteReason: 'max_output_tokens' };
332
+ if (value === 'max_tokens') return { status: 'incomplete', incompleteReason: 'max_output_tokens' };
333
+ if (value === 'model_context_window_exceeded') return { status: 'incomplete', incompleteReason: 'context_window_exceeded' };
293
334
  if (value === 'refusal') return { status: 'incomplete', incompleteReason: 'content_filter' };
294
335
  return { status: 'completed', incompleteReason: null };
295
336
  }
@@ -14,18 +14,28 @@ const { writeConvertedResponseAsSse } = require('./chatBridgeResponse');
14
14
  const { classifyResponsesRoute } = require('./providerBridgeRoutes');
15
15
  const { providerError } = require('./providerProfile');
16
16
  const { sanitizeProviderMessage } = require('./providerRedaction');
17
+ const { logDebug } = require('./debugLog');
18
+ const {
19
+ createProviderStreamLifecycle,
20
+ resolveProviderIdleTimeoutMs,
21
+ resolveProviderTotalTimeoutMs
22
+ } = require('./providerStreamLifecycle');
17
23
 
18
24
  const MAX_REQUEST_BYTES = 16 * 1024 * 1024;
19
25
  const MAX_ERROR_BYTES = 64 * 1024;
20
26
  const MAX_HISTORY_ENTRIES = 64;
27
+ const MAX_CONTINUATION_CHARS = 512 * 1024;
28
+ const MAX_REASONING_CONTINUATIONS = 2;
29
+ const REASONING_CONTINUATION_MARKER = '[codex-overleaf-provider-reasoning-continuation]';
21
30
 
22
31
  async function startAnthropicMessagesBridge({ launch, signal } = {}) {
23
32
  if (!launch?.baseUrl) throw providerError('provider_base_url_invalid', 'Anthropic routing requires a provider Base URL.');
24
33
  const clientToken = crypto.randomBytes(32).toString('base64url');
25
34
  const activeRequests = new Set();
26
35
  const history = new Map();
36
+ const continuationState = { anchorMessages: [], attempts: 0, responseId: '' };
27
37
  const server = http.createServer((req, res) => {
28
- handleRequest({ req, res, launch, clientToken, activeRequests, history }).catch(error => {
38
+ handleRequest({ req, res, launch, clientToken, activeRequests, history, continuationState }).catch(error => {
29
39
  if (res.writableEnded) return;
30
40
  const code = normalizeBridgeErrorCode(error?.code);
31
41
  const message = sanitizeProviderMessage(error?.message, [launch.apiKey]) || 'The Anthropic provider bridge failed.';
@@ -40,9 +50,10 @@ async function startAnthropicMessagesBridge({ launch, signal } = {}) {
40
50
  }
41
51
  });
42
52
  });
43
- const timeoutMs = resolveTimeout(launch);
53
+ const idleTimeoutMs = resolveProviderIdleTimeoutMs(launch);
54
+ const totalTimeoutMs = resolveProviderTotalTimeoutMs(launch, idleTimeoutMs);
44
55
  server.keepAliveTimeout = 5000;
45
- server.requestTimeout = timeoutMs + 5000;
56
+ server.requestTimeout = totalTimeoutMs + 5000;
46
57
  await listen(server);
47
58
  const address = server.address();
48
59
  let closed = false;
@@ -53,6 +64,9 @@ async function startAnthropicMessagesBridge({ launch, signal } = {}) {
53
64
  for (const controller of activeRequests) controller.abort();
54
65
  activeRequests.clear();
55
66
  history.clear();
67
+ continuationState.anchorMessages = [];
68
+ continuationState.attempts = 0;
69
+ continuationState.responseId = '';
56
70
  await closeServer(server);
57
71
  };
58
72
  signal?.addEventListener('abort', close, { once: true });
@@ -60,7 +74,7 @@ async function startAnthropicMessagesBridge({ launch, signal } = {}) {
60
74
  return { baseUrl: `http://127.0.0.1:${address.port}/v1`, clientToken, close };
61
75
  }
62
76
 
63
- async function handleRequest({ req, res, launch, clientToken, activeRequests, history }) {
77
+ async function handleRequest({ req, res, launch, clientToken, activeRequests, history, continuationState }) {
64
78
  if (!isAuthorized(req, clientToken)) {
65
79
  sendJsonError(res, 401, 'unauthorized', 'Local bridge authorization failed.');
66
80
  return;
@@ -76,22 +90,48 @@ async function handleRequest({ req, res, launch, clientToken, activeRequests, hi
76
90
  return;
77
91
  }
78
92
  const requestBody = await readJsonBody(req);
79
- const previous = history.get(String(requestBody.previous_response_id || ''));
93
+ const previousResponseId = String(requestBody.previous_response_id || '');
94
+ const previous = history.get(previousResponseId);
95
+ const fallbackAnchor = previous ? [] : continuationState.anchorMessages;
96
+ const continuing = Boolean(previous?.continuation || fallbackAnchor.length);
97
+ if (continuing && continuationState.attempts >= MAX_REASONING_CONTINUATIONS) {
98
+ throw providerError(
99
+ 'provider_reasoning_continuation_exhausted',
100
+ 'The provider repeatedly interrupted the same reasoning stream. Retry the task or use a different model endpoint.'
101
+ );
102
+ }
80
103
  const translated = buildAnthropicRequest({
81
104
  requestBody,
82
105
  launch,
83
- historyMessages: previous?.messages || []
106
+ historyMessages: previous?.messages || fallbackAnchor
107
+ });
108
+ logDebug('provider.anthropic.request', {
109
+ model: translated.body.model,
110
+ stream: requestBody.stream !== false,
111
+ maxTokens: translated.body.max_tokens,
112
+ thinkingType: translated.body.thinking?.type || 'disabled',
113
+ hasPreviousResponse: Boolean(previousResponseId),
114
+ continuing,
115
+ continuationAttempt: continuationState.attempts
84
116
  });
85
117
  const controller = new AbortController();
86
118
  activeRequests.add(controller);
119
+ let abortReason = '';
120
+ const lifecycle = createProviderStreamLifecycle({
121
+ controller,
122
+ idleTimeoutMs: resolveProviderIdleTimeoutMs(launch),
123
+ totalTimeoutMs: resolveProviderTotalTimeoutMs(launch),
124
+ onAbort: reason => { abortReason = reason; }
125
+ });
87
126
  const onClientClose = () => {
88
- if (!res.writableEnded) controller.abort();
127
+ if (!res.writableEnded) lifecycle.abort('client_close');
89
128
  };
90
129
  req.on('aborted', onClientClose);
91
130
  res.on('close', onClientClose);
92
- const timeout = setTimeout(() => controller.abort(), resolveTimeout(launch));
131
+ lifecycle.start();
93
132
  try {
94
133
  const upstream = await fetchWithRectification(launch, translated.body, controller.signal);
134
+ lifecycle.touch();
95
135
  if (!upstream.response.ok) {
96
136
  forwardUpstreamError(upstream.response, upstream.errorText, res, launch.apiKey);
97
137
  return;
@@ -102,14 +142,48 @@ async function handleRequest({ req, res, launch, clientToken, activeRequests, hi
102
142
  model: translated.body.model,
103
143
  toolContext: translated.toolContext
104
144
  };
105
- const remember = converted => rememberHistory(history, converted.response.id, [
106
- ...(upstream.requestBody.messages || translated.messages),
107
- { role: 'assistant', content: converted.assistantBlocks }
108
- ]);
145
+ const remember = converted => {
146
+ const baseMessages = upstream.requestBody.messages || translated.messages;
147
+ const continuation = buildAnthropicHistoryContinuation(converted);
148
+ const anchored = continuation.length > 0;
149
+ const assistantMessages = anchored
150
+ ? continuation
151
+ : [{ role: 'assistant', content: converted.assistantBlocks }];
152
+ const messages = [...baseMessages, ...assistantMessages];
153
+ if (anchored) {
154
+ continuationState.anchorMessages = messages;
155
+ continuationState.attempts += 1;
156
+ continuationState.responseId = converted.response.id;
157
+ } else {
158
+ continuationState.anchorMessages = [];
159
+ continuationState.attempts = 0;
160
+ continuationState.responseId = '';
161
+ }
162
+ rememberHistory(history, converted.response.id, messages, { continuation: anchored });
163
+ const blocks = Array.isArray(converted.assistantBlocks) ? converted.assistantBlocks : [];
164
+ logDebug('provider.anthropic.response', {
165
+ responseId: converted.response.id,
166
+ status: converted.response.status,
167
+ incompleteReason: converted.response.incomplete_details?.reason || '',
168
+ blockTypes: blocks.map(block => String(block?.type || 'unknown')),
169
+ textChars: blocks.reduce((total, block) => total + (block?.type === 'text' ? String(block.text || '').length : 0), 0),
170
+ thinkingChars: blocks.reduce((total, block) => total + (block?.type === 'thinking' ? String(block.thinking || '').length : 0), 0),
171
+ signedThinkingBlocks: blocks.filter(block => block?.type === 'thinking' && block.signature).length,
172
+ continuationAnchored: anchored,
173
+ continuationAttempt: continuationState.attempts
174
+ });
175
+ };
109
176
  const contentType = String(upstream.response.headers.get('content-type') || '').toLowerCase();
110
177
  if (requestBody.stream !== false) {
111
178
  if (contentType.includes('text/event-stream')) {
112
- await streamAnthropicResponse({ upstream: upstream.response, res, context, onComplete: remember });
179
+ await streamAnthropicResponse({
180
+ upstream: upstream.response,
181
+ res,
182
+ context,
183
+ onComplete: remember,
184
+ onActivity: lifecycle.touch,
185
+ recoverInterrupted: lifecycle.recoverInterrupted
186
+ });
113
187
  } else {
114
188
  const converted = convertAnthropicResponse(await upstream.response.json(), context);
115
189
  writeConvertedResponseAsSse(res, converted, context, remember);
@@ -117,7 +191,14 @@ async function handleRequest({ req, res, launch, clientToken, activeRequests, hi
117
191
  } else if (contentType.includes('text/event-stream')) {
118
192
  let converted;
119
193
  const sink = { writeHead() {}, write() {}, end() {} };
120
- await streamAnthropicResponse({ upstream: upstream.response, res: sink, context, onComplete: value => { converted = value; } });
194
+ await streamAnthropicResponse({
195
+ upstream: upstream.response,
196
+ res: sink,
197
+ context,
198
+ onComplete: value => { converted = value; },
199
+ onActivity: lifecycle.touch,
200
+ recoverInterrupted: lifecycle.recoverInterrupted
201
+ });
121
202
  remember(converted);
122
203
  sendJson(res, 200, converted.response);
123
204
  } else {
@@ -126,10 +207,17 @@ async function handleRequest({ req, res, launch, clientToken, activeRequests, hi
126
207
  sendJson(res, 200, converted.response);
127
208
  }
128
209
  } catch (error) {
129
- if (controller.signal.aborted) throw providerError('provider_connection_timeout', 'The provider request was cancelled or timed out.');
210
+ if (controller.signal.aborted) {
211
+ throw providerError(
212
+ 'provider_connection_timeout',
213
+ abortReason === 'total_timeout'
214
+ ? 'The provider exceeded the maximum total request duration.'
215
+ : 'The provider stopped producing data before the request completed.'
216
+ );
217
+ }
130
218
  throw error;
131
219
  } finally {
132
- clearTimeout(timeout);
220
+ lifecycle.dispose();
133
221
  activeRequests.delete(controller);
134
222
  req.removeListener('aborted', onClientClose);
135
223
  res.removeListener('close', onClientClose);
@@ -202,20 +290,64 @@ function forwardUpstreamError(response, errorText, res, apiKey) {
202
290
  sendJsonError(res, response.status, 'provider_upstream_error', sanitizeProviderMessage(message, [apiKey]));
203
291
  }
204
292
 
205
- function rememberHistory(history, responseId, messages) {
293
+ function rememberHistory(history, responseId, messages, metadata = {}) {
206
294
  history.delete(responseId);
207
- history.set(responseId, { messages });
295
+ history.set(responseId, { messages, ...metadata });
208
296
  while (history.size > MAX_HISTORY_ENTRIES) history.delete(history.keys().next().value);
209
297
  }
210
298
 
211
- function resolveTimeout(launch = {}) {
212
- const configured = Math.min(300000, Math.max(5000, Number(launch.requestTimeoutMs) || 30000));
213
- const thinking = !['', 'none', 'off', 'disabled'].includes(String(launch.reasoningEffort || '').toLowerCase());
214
- return thinking ? Math.max(120000, configured) : configured;
299
+ function buildAnthropicHistoryContinuation(converted = {}) {
300
+ if (converted.response?.status !== 'incomplete') return [];
301
+ if (converted.response?.incomplete_details?.reason !== 'max_output_tokens') return [];
302
+ const blocks = Array.isArray(converted.assistantBlocks) ? converted.assistantBlocks : [];
303
+ if (blocks.some(block => block?.type === 'tool_use')) return [];
304
+ const assistantContent = [];
305
+ const unsignedThinking = [];
306
+ for (const block of blocks) {
307
+ if (block?.type === 'thinking') {
308
+ const thinking = String(block.thinking || '');
309
+ const signature = String(block.signature || '');
310
+ if (signature) assistantContent.push({ type: 'thinking', thinking, signature });
311
+ else if (thinking) unsignedThinking.push(thinking);
312
+ continue;
313
+ }
314
+ if (block?.type === 'redacted_thinking' && block.data) {
315
+ assistantContent.push({ type: 'redacted_thinking', data: block.data });
316
+ continue;
317
+ }
318
+ if (block?.type === 'text' && block.text) {
319
+ assistantContent.push({ type: 'text', text: String(block.text) });
320
+ }
321
+ }
322
+ if (!assistantContent.length && !unsignedThinking.length) return [];
323
+
324
+ const messages = [];
325
+ if (assistantContent.length) messages.push({ role: 'assistant', content: assistantContent });
326
+ const partial = unsignedThinking.join('\n\n').trim();
327
+ const retained = partial.length > MAX_CONTINUATION_CHARS
328
+ ? partial.slice(-MAX_CONTINUATION_CHARS)
329
+ : partial;
330
+ const preservedTail = retained
331
+ ? `\n\nPreserved unfinished reasoning tail:\n${retained}`
332
+ : '';
333
+ messages.push({
334
+ role: 'user',
335
+ content: [{
336
+ type: 'text',
337
+ text: `${REASONING_CONTINUATION_MARKER}\nThe previous provider response reached its output limit. Continue from the exact unfinished point. Do not reread files already analyzed, repeat prior analysis, restate the plan, or restart the task. Complete the remaining work and produce the final answer.${preservedTail}`
338
+ }]
339
+ });
340
+ return messages;
215
341
  }
216
342
 
217
343
  function normalizeBridgeErrorCode(value) {
218
- return ['provider_connection_timeout', 'provider_response_invalid', 'provider_request_invalid', 'provider_upstream_error'].includes(value)
344
+ return [
345
+ 'provider_connection_timeout',
346
+ 'provider_reasoning_continuation_exhausted',
347
+ 'provider_response_invalid',
348
+ 'provider_request_invalid',
349
+ 'provider_upstream_error'
350
+ ].includes(value)
219
351
  ? value
220
352
  : 'provider_bridge_failed';
221
353
  }
@@ -275,6 +407,7 @@ function closeServer(server) {
275
407
  }
276
408
 
277
409
  module.exports = {
410
+ buildAnthropicHistoryContinuation,
278
411
  buildAnthropicHeaders,
279
412
  buildAnthropicMessagesUrl,
280
413
  startAnthropicMessagesBridge
@@ -36,7 +36,7 @@ function buildChatRequest({ requestBody = {}, launch = {}, historyMessages = []
36
36
  copyNumber(requestBody, body, 'temperature');
37
37
  copyNumber(requestBody, body, 'top_p');
38
38
  const maxOutputTokens = resolveReasoningMaxOutputTokens(
39
- requestBody.max_output_tokens ?? launch.maxOutputTokens,
39
+ launch.maxOutputTokens ?? requestBody.max_output_tokens,
40
40
  requestBody,
41
41
  launch,
42
42
  model
@@ -2,6 +2,7 @@
2
2
 
3
3
  const crypto = require('node:crypto');
4
4
  const { requiresReasoningContentReplay } = require('./providerReasoning');
5
+ const MAX_STREAM_BUFFER_BYTES = 8 * 1024 * 1024;
5
6
 
6
7
  function convertChatResponse(chat = {}, context = {}) {
7
8
  const choice = Array.isArray(chat.choices) ? chat.choices[0] || {} : {};
@@ -56,6 +57,9 @@ async function streamChatResponse({
56
57
  for await (const chunk of upstream.body || []) {
57
58
  onActivity();
58
59
  buffer += decoder.decode(chunk, { stream: true });
60
+ if (Buffer.byteLength(buffer, 'utf8') > MAX_STREAM_BUFFER_BYTES) {
61
+ throw responseError('Provider stream frame exceeded the local bridge limit.', 'provider_stream_frame_too_large');
62
+ }
59
63
  let boundary = findSseBoundary(buffer);
60
64
  while (boundary) {
61
65
  const block = buffer.slice(0, boundary.index);
@@ -240,7 +244,7 @@ function completeStream(state, res, interruptedReason = '') {
240
244
  const tools = Array.from(state.tools.values()).sort((a, b) => a.index - b.index);
241
245
  for (const tool of tools) completeTool(state, res, tool);
242
246
  const terminal = interruptedReason
243
- ? { status: 'incomplete', incompleteReason: 'max_output_tokens' }
247
+ ? { status: 'incomplete', incompleteReason: interruptedReason }
244
248
  : mapChatFinishReason(state.finishReason);
245
249
  const response = streamEnvelope(state, terminal.status, terminal.incompleteReason);
246
250
  emit(
@@ -470,24 +474,24 @@ function extractReasoning(message = {}) {
470
474
 
471
475
  function mapChatFinishReason(value) {
472
476
  const reason = normalizeText(value).trim().toLowerCase();
473
- if (['length', 'max_tokens', 'max_output_tokens', 'model_context_window_exceeded'].includes(reason)) {
477
+ if (['length', 'max_tokens', 'max_output_tokens'].includes(reason)) {
474
478
  return { status: 'incomplete', incompleteReason: 'max_output_tokens' };
475
479
  }
480
+ if (reason === 'model_context_window_exceeded') {
481
+ return { status: 'incomplete', incompleteReason: 'context_window_exceeded' };
482
+ }
476
483
  if (['content_filter', 'content-filter', 'refusal', 'safety'].includes(reason)) {
477
484
  return { status: 'incomplete', incompleteReason: 'content_filter' };
478
485
  }
479
486
  if (['insufficient_system_resource', 'insufficient-system-resource', 'resource_exhausted'].includes(reason)) {
480
- // The Responses API has no portable provider-resource incomplete reason.
481
- // Treat the partial generation like an output truncation so Codex can
482
- // continue it through the bounded history-anchor path.
483
- return { status: 'incomplete', incompleteReason: 'max_output_tokens' };
487
+ return { status: 'incomplete', incompleteReason: 'provider_resource_exhausted' };
484
488
  }
485
489
  return { status: 'completed', incompleteReason: null };
486
490
  }
487
491
 
488
- function responseError(message) {
492
+ function responseError(message, code = 'provider_response_invalid') {
489
493
  const error = new Error(message);
490
- error.code = 'provider_response_invalid';
494
+ error.code = code;
491
495
  return error;
492
496
  }
493
497
 
@@ -13,6 +13,11 @@ const { classifyResponsesRoute } = require('./providerBridgeRoutes');
13
13
  const { providerError } = require('./providerProfile');
14
14
  const { sanitizeProviderMessage } = require('./providerRedaction');
15
15
  const { logDebug } = require('./debugLog');
16
+ const {
17
+ createProviderStreamLifecycle,
18
+ resolveProviderIdleTimeoutMs,
19
+ resolveProviderTotalTimeoutMs
20
+ } = require('./providerStreamLifecycle');
16
21
 
17
22
  const MAX_REQUEST_BYTES = 16 * 1024 * 1024;
18
23
  const MAX_ERROR_BYTES = 64 * 1024;
@@ -57,9 +62,10 @@ async function startChatCompletionsBridge({ launch, signal } = {}) {
57
62
  }
58
63
  });
59
64
  });
60
- const requestTimeoutMs = resolveRequestTimeoutMs(launch);
65
+ const requestTimeoutMs = resolveProviderIdleTimeoutMs(launch);
66
+ const totalTimeoutMs = resolveProviderTotalTimeoutMs(launch, requestTimeoutMs);
61
67
  server.keepAliveTimeout = 5000;
62
- server.requestTimeout = requestTimeoutMs + 5000;
68
+ server.requestTimeout = totalTimeoutMs + 5000;
63
69
  await listen(server);
64
70
  const address = server.address();
65
71
  const baseUrl = `http://127.0.0.1:${address.port}/v1`;
@@ -121,8 +127,8 @@ async function handleRequest({
121
127
  historyMessages: previous?.messages || fallbackAnchor
122
128
  });
123
129
  assertReasoningContinuationBudget(translated.messages, continuationState.attempts);
124
- const idleTimeoutMs = resolveRequestTimeoutMs(launch);
125
- const totalTimeoutMs = resolveTotalRequestTimeoutMs(launch, idleTimeoutMs);
130
+ const idleTimeoutMs = resolveProviderIdleTimeoutMs(launch);
131
+ const totalTimeoutMs = resolveProviderTotalTimeoutMs(launch, idleTimeoutMs);
126
132
  logDebug('provider.bridge.request', {
127
133
  traceId,
128
134
  providerName: launch.providerName,
@@ -142,23 +148,18 @@ async function handleRequest({
142
148
  const controller = new AbortController();
143
149
  activeRequests.add(controller);
144
150
  let abortReason = '';
145
- let idleTimeout;
146
- const abortRequest = reason => {
147
- if (controller.signal.aborted) return;
148
- abortReason = reason;
149
- controller.abort();
150
- };
151
- const refreshIdleTimeout = () => {
152
- clearTimeout(idleTimeout);
153
- idleTimeout = setTimeout(() => abortRequest('idle_timeout'), idleTimeoutMs);
154
- };
151
+ const lifecycle = createProviderStreamLifecycle({
152
+ controller,
153
+ idleTimeoutMs,
154
+ totalTimeoutMs,
155
+ onAbort: reason => { abortReason = reason; }
156
+ });
155
157
  const onClientClose = () => {
156
- if (!res.writableEnded) abortRequest('client_close');
158
+ if (!res.writableEnded) lifecycle.abort('client_close');
157
159
  };
158
160
  req.on('aborted', onClientClose);
159
161
  res.on('close', onClientClose);
160
- refreshIdleTimeout();
161
- const totalTimeout = setTimeout(() => abortRequest('total_timeout'), totalTimeoutMs);
162
+ lifecycle.start();
162
163
  try {
163
164
  const upstream = await fetch(buildChatCompletionsUrl(launch.baseUrl, launch), {
164
165
  method: 'POST',
@@ -166,7 +167,7 @@ async function handleRequest({
166
167
  body: JSON.stringify(translated.body),
167
168
  signal: controller.signal
168
169
  });
169
- refreshIdleTimeout();
170
+ lifecycle.touch();
170
171
  if (!upstream.ok) {
171
172
  await forwardUpstreamError(upstream, res, launch.apiKey);
172
173
  return;
@@ -226,12 +227,8 @@ async function handleRequest({
226
227
  res,
227
228
  context,
228
229
  onComplete: remember,
229
- onActivity: refreshIdleTimeout,
230
- recoverInterrupted: () => (
231
- ['idle_timeout', 'total_timeout'].includes(abortReason)
232
- ? abortReason
233
- : ''
234
- )
230
+ onActivity: lifecycle.touch,
231
+ recoverInterrupted: lifecycle.recoverInterrupted
235
232
  });
236
233
  } else {
237
234
  const converted = convertChatResponse(await upstream.json(), context);
@@ -255,8 +252,7 @@ async function handleRequest({
255
252
  }
256
253
  throw error;
257
254
  } finally {
258
- clearTimeout(idleTimeout);
259
- clearTimeout(totalTimeout);
255
+ lifecycle.dispose();
260
256
  activeRequests.delete(controller);
261
257
  req.removeListener('aborted', onClientClose);
262
258
  res.removeListener('close', onClientClose);
@@ -309,20 +305,6 @@ function buildUpstreamHeaders(launch = {}) {
309
305
  return headers;
310
306
  }
311
307
 
312
- function resolveRequestTimeoutMs(launch = {}) {
313
- const configured = Math.min(300000, Math.max(5000, Number(launch.requestTimeoutMs) || 30000));
314
- const deepSeekThinking = requiresReasoningContentReplay(launch, launch.modelId)
315
- && String(launch.reasoningEffort || '').toLowerCase() !== 'none';
316
- return deepSeekThinking ? Math.max(120000, configured) : configured;
317
- }
318
-
319
- function resolveTotalRequestTimeoutMs(launch = {}, idleTimeoutMs = resolveRequestTimeoutMs(launch)) {
320
- const deepSeekThinking = requiresReasoningContentReplay(launch, launch.modelId)
321
- && String(launch.reasoningEffort || '').toLowerCase() !== 'none';
322
- const floor = deepSeekThinking ? 30 * 60 * 1000 : 5 * 60 * 1000;
323
- return Math.max(floor, idleTimeoutMs * 2);
324
- }
325
-
326
308
  function buildHistoryContinuation(converted = {}) {
327
309
  const assistant = converted.assistantMessage || {};
328
310
  const reasoning = typeof assistant.reasoning_content === 'string'
@@ -268,7 +268,9 @@ function formatMathOutputRules() {
268
268
  return [
269
269
  '- In user-visible prose, format inline mathematics as $...$ and display mathematics as $$...$$ on separate lines.',
270
270
  '- Never wrap mathematical expressions in backticks or fenced code blocks unless you are showing literal source code.',
271
- '- Do not emit bare TeX commands as prose when explicit math delimiters can express the same formula.'
271
+ '- Do not emit bare TeX commands as prose when explicit math delimiters can express the same formula.',
272
+ '- Keep prose outside math delimiters. Inside mathematics, wrap necessary words with \\text{...} and use \\mid for conditional bars.',
273
+ '- For long display equations, use \\begin{aligned}...\\end{aligned} and place explicit row breaks near relation operators.'
272
274
  ].join('\n');
273
275
  }
274
276
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { startChatCompletionsBridge } = require('./chatCompletionsBridge');
4
4
  const { startAnthropicMessagesBridge } = require('./anthropicMessagesBridge');
5
+ const { startResponsesPassthroughBridge } = require('./responsesPassthroughBridge');
5
6
  const { prepareProviderModelCatalog } = require('./providerModelCatalog');
6
7
 
7
8
  const PROVIDER_CONFIG_ID = 'codex_overleaf_custom';
@@ -49,21 +50,23 @@ function createProviderLaunch({ profile, secret = '', modelId, wireApi, reasonin
49
50
  anthropicThinkingMode: profile.anthropicThinkingMode || 'budget',
50
51
  anthropicPromptCaching: profile.anthropicPromptCaching === true,
51
52
  impersonateClaudeCode: profile.impersonateClaudeCode === true,
52
- maxOutputTokens: profile.maxOutputTokens || 32768,
53
+ maxOutputTokens: profile.maxOutputTokens || 65536,
53
54
  apiKey: secret
54
55
  });
55
56
  }
56
57
 
57
58
  async function prepareProviderLaunch(launch, { signal } = {}) {
58
59
  const catalog = prepareProviderModelCatalog(launch);
59
- if (!catalog.launch || !['chat', 'anthropic'].includes(catalog.launch.wireApi)) {
60
+ if (!catalog.launch || !['responses', 'chat', 'anthropic'].includes(catalog.launch.wireApi)) {
60
61
  return { launch: catalog.launch, close: async () => catalog.close() };
61
62
  }
62
63
  try {
63
64
  const routedWireApi = catalog.launch.wireApi;
64
65
  const bridge = routedWireApi === 'anthropic'
65
66
  ? await startAnthropicMessagesBridge({ launch: catalog.launch, signal })
66
- : await startChatCompletionsBridge({ launch: catalog.launch, signal });
67
+ : routedWireApi === 'chat'
68
+ ? await startChatCompletionsBridge({ launch: catalog.launch, signal })
69
+ : await startResponsesPassthroughBridge({ launch: catalog.launch, signal });
67
70
  return {
68
71
  launch: Object.freeze({
69
72
  ...catalog.launch,