codex-overleaf-link 2.0.0 → 2.1.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 (119) hide show
  1. package/README.md +24 -23
  2. package/extension/bootstrap/manifest.template.json +2 -1
  3. package/extension/runtime-manifest.json +12 -6
  4. package/extension/src/background.js +7 -0
  5. package/extension/src/content/composerPanel.js +13 -3
  6. package/extension/src/content/contentRuntime.js +129 -64
  7. package/extension/src/content/contextTray.js +173 -9
  8. package/extension/src/content/markdownText.js +16 -1
  9. package/extension/src/content/mathText.js +233 -0
  10. package/extension/src/content/modelPicker.js +117 -106
  11. package/extension/src/content/modelPickerSupport.js +115 -0
  12. package/extension/src/content/nativeChannel.js +20 -1
  13. package/extension/src/content/panelMaintenance.js +2 -0
  14. package/extension/src/content/projectSettingsCoordinator.js +2 -0
  15. package/extension/src/content/providerSettingsCoordinator.js +413 -0
  16. package/extension/src/content/providerSettingsDialog.js +1006 -0
  17. package/extension/src/content/runTimelineView.js +24 -5
  18. package/extension/src/content/settingsPanel.js +33 -2
  19. package/extension/src/content/writebackOrchestrator.js +3 -1
  20. package/extension/src/page/overleafProjectSnapshot.js +29 -33
  21. package/extension/src/page/snapshotRouter.js +75 -8
  22. package/extension/src/page/writebackRouter.js +20 -33
  23. package/extension/src/pageBridge.js +1 -1
  24. package/extension/src/shared/agentTranscript.js +130 -0
  25. package/extension/src/shared/compatibility.js +3 -2
  26. package/extension/src/shared/failureReasons.js +75 -0
  27. package/extension/src/shared/i18n.js +22 -0
  28. package/extension/src/shared/models.js +1 -0
  29. package/extension/src/shared/providerProfiles.js +283 -0
  30. package/extension/src/shared/sessionState.js +12 -2
  31. package/extension/src/shared/storageDb.js +1 -1
  32. package/extension/styles/panel.css +689 -1
  33. package/extension/vendor/katex/LICENSE +21 -0
  34. package/extension/vendor/katex/VERSION +2 -0
  35. package/extension/vendor/katex/fonts/KaTeX_AMS-Regular.ttf +0 -0
  36. package/extension/vendor/katex/fonts/KaTeX_AMS-Regular.woff +0 -0
  37. package/extension/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  38. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  39. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  40. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  41. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  42. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  43. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  44. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  45. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  46. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  47. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  48. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  49. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  50. package/extension/vendor/katex/fonts/KaTeX_Main-Bold.ttf +0 -0
  51. package/extension/vendor/katex/fonts/KaTeX_Main-Bold.woff +0 -0
  52. package/extension/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  53. package/extension/vendor/katex/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  54. package/extension/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  55. package/extension/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  56. package/extension/vendor/katex/fonts/KaTeX_Main-Italic.ttf +0 -0
  57. package/extension/vendor/katex/fonts/KaTeX_Main-Italic.woff +0 -0
  58. package/extension/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  59. package/extension/vendor/katex/fonts/KaTeX_Main-Regular.ttf +0 -0
  60. package/extension/vendor/katex/fonts/KaTeX_Main-Regular.woff +0 -0
  61. package/extension/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  62. package/extension/vendor/katex/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  63. package/extension/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  64. package/extension/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  65. package/extension/vendor/katex/fonts/KaTeX_Math-Italic.ttf +0 -0
  66. package/extension/vendor/katex/fonts/KaTeX_Math-Italic.woff +0 -0
  67. package/extension/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  68. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  69. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  70. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  71. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  72. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  73. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  74. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  75. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  76. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  77. package/extension/vendor/katex/fonts/KaTeX_Script-Regular.ttf +0 -0
  78. package/extension/vendor/katex/fonts/KaTeX_Script-Regular.woff +0 -0
  79. package/extension/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  80. package/extension/vendor/katex/fonts/KaTeX_Size1-Regular.ttf +0 -0
  81. package/extension/vendor/katex/fonts/KaTeX_Size1-Regular.woff +0 -0
  82. package/extension/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  83. package/extension/vendor/katex/fonts/KaTeX_Size2-Regular.ttf +0 -0
  84. package/extension/vendor/katex/fonts/KaTeX_Size2-Regular.woff +0 -0
  85. package/extension/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  86. package/extension/vendor/katex/fonts/KaTeX_Size3-Regular.ttf +0 -0
  87. package/extension/vendor/katex/fonts/KaTeX_Size3-Regular.woff +0 -0
  88. package/extension/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  89. package/extension/vendor/katex/fonts/KaTeX_Size4-Regular.ttf +0 -0
  90. package/extension/vendor/katex/fonts/KaTeX_Size4-Regular.woff +0 -0
  91. package/extension/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  92. package/extension/vendor/katex/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  93. package/extension/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  94. package/extension/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  95. package/extension/vendor/katex/katex.min.css +1 -0
  96. package/extension/vendor/katex/katex.min.js +1 -0
  97. package/native-host/src/anthropicBridgeRequest.js +435 -0
  98. package/native-host/src/anthropicBridgeResponse.js +431 -0
  99. package/native-host/src/anthropicMessagesBridge.js +281 -0
  100. package/native-host/src/chatBridgeRequest.js +274 -0
  101. package/native-host/src/chatBridgeResponse.js +594 -0
  102. package/native-host/src/chatCompletionsBridge.js +485 -0
  103. package/native-host/src/codexCommand.js +25 -0
  104. package/native-host/src/codexPromptAssembly.js +9 -0
  105. package/native-host/src/codexProviderLaunch.js +213 -0
  106. package/native-host/src/codexProviderTest.js +196 -0
  107. package/native-host/src/codexSessionRunner.js +29 -25
  108. package/native-host/src/managedInstall.js +1 -0
  109. package/native-host/src/providerBridgeRoutes.js +11 -0
  110. package/native-host/src/providerModelCatalog.js +107 -0
  111. package/native-host/src/providerProfile.js +454 -0
  112. package/native-host/src/providerReasoning.js +235 -0
  113. package/native-host/src/providerRedaction.js +18 -0
  114. package/native-host/src/providerRuntime.js +365 -0
  115. package/native-host/src/providerStore.js +691 -0
  116. package/native-host/src/readProgressGuard.js +360 -0
  117. package/native-host/src/taskRunnerRuntime.js +39 -3
  118. package/native-host/src/updateArchive.js +1 -0
  119. package/package.json +2 -1
@@ -0,0 +1,485 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const http = require('node:http');
5
+ const { buildChatRequest } = require('./chatBridgeRequest');
6
+ const {
7
+ convertChatResponse,
8
+ streamChatResponse,
9
+ writeConvertedResponseAsSse
10
+ } = require('./chatBridgeResponse');
11
+ const { requiresReasoningContentReplay } = require('./providerReasoning');
12
+ const { classifyResponsesRoute } = require('./providerBridgeRoutes');
13
+ const { providerError } = require('./providerProfile');
14
+ const { sanitizeProviderMessage } = require('./providerRedaction');
15
+ const { logDebug } = require('./debugLog');
16
+
17
+ const MAX_REQUEST_BYTES = 16 * 1024 * 1024;
18
+ const MAX_ERROR_BYTES = 64 * 1024;
19
+ const MAX_HISTORY_ENTRIES = 64;
20
+ const MAX_REASONING_CONTINUATIONS = 2;
21
+ const REASONING_CONTINUATION_MARKER = '[codex-overleaf-provider-reasoning-continuation]';
22
+
23
+ async function startChatCompletionsBridge({ launch, signal } = {}) {
24
+ if (!launch?.baseUrl) {
25
+ throw providerError('provider_base_url_invalid', 'Chat routing requires a provider Base URL.');
26
+ }
27
+ const clientToken = crypto.randomBytes(32).toString('base64url');
28
+ const activeRequests = new Set();
29
+ const history = new Map();
30
+ const continuationState = {
31
+ anchorMessages: [],
32
+ attempts: 0,
33
+ responseId: ''
34
+ };
35
+ const server = http.createServer((req, res) => {
36
+ handleRequest({
37
+ req,
38
+ res,
39
+ launch,
40
+ clientToken,
41
+ activeRequests,
42
+ history,
43
+ continuationState
44
+ }).catch(error => {
45
+ if (res.writableEnded) return;
46
+ const code = normalizeBridgeErrorCode(error?.code);
47
+ const message = sanitizeProviderMessage(error?.message, [launch.apiKey]) || bridgeErrorMessage(code);
48
+ if (res.headersSent) {
49
+ const payload = {
50
+ type: 'response.failed',
51
+ sequence_number: 999999,
52
+ response: { status: 'failed', error: { code, message } }
53
+ };
54
+ res.end(`event: response.failed\ndata: ${JSON.stringify(payload)}\n\n`);
55
+ } else {
56
+ sendJsonError(res, code === 'provider_connection_timeout' ? 504 : 502, code, message);
57
+ }
58
+ });
59
+ });
60
+ const requestTimeoutMs = resolveRequestTimeoutMs(launch);
61
+ server.keepAliveTimeout = 5000;
62
+ server.requestTimeout = requestTimeoutMs + 5000;
63
+ await listen(server);
64
+ const address = server.address();
65
+ const baseUrl = `http://127.0.0.1:${address.port}/v1`;
66
+ let closed = false;
67
+ const close = async () => {
68
+ if (closed) return;
69
+ closed = true;
70
+ signal?.removeEventListener('abort', close);
71
+ for (const controller of activeRequests) controller.abort();
72
+ activeRequests.clear();
73
+ history.clear();
74
+ continuationState.anchorMessages = [];
75
+ continuationState.attempts = 0;
76
+ continuationState.responseId = '';
77
+ await closeServer(server);
78
+ };
79
+ signal?.addEventListener('abort', close, { once: true });
80
+ if (signal?.aborted) await close();
81
+ return { baseUrl, clientToken, close };
82
+ }
83
+
84
+ async function handleRequest({
85
+ req,
86
+ res,
87
+ launch,
88
+ clientToken,
89
+ activeRequests,
90
+ history,
91
+ continuationState
92
+ }) {
93
+ if (!isAuthorized(req, clientToken)) {
94
+ sendJsonError(res, 401, 'unauthorized', 'Local bridge authorization failed.');
95
+ return;
96
+ }
97
+ const url = new URL(req.url || '/', 'http://127.0.0.1');
98
+ if (req.method === 'GET' && /\/models\/?$/.test(url.pathname)) {
99
+ sendJson(res, 200, {
100
+ object: 'list',
101
+ data: [{ id: launch.modelId, object: 'model', created: 0, owned_by: launch.providerName || 'custom' }]
102
+ });
103
+ return;
104
+ }
105
+ const responsesRoute = classifyResponsesRoute(req.method, url.pathname);
106
+ if (!responsesRoute) {
107
+ sendJsonError(res, 404, 'not_found', 'The local provider bridge only exposes Responses, Responses Compact, and Models endpoints.');
108
+ return;
109
+ }
110
+ const requestBody = await readJsonBody(req);
111
+ const traceId = crypto.randomUUID().slice(0, 12);
112
+ const startedAt = Date.now();
113
+ const previousResponseId = String(requestBody.previous_response_id || '');
114
+ const previous = history.get(previousResponseId);
115
+ const fallbackAnchor = previous
116
+ ? []
117
+ : continuationState.anchorMessages;
118
+ const translated = buildChatRequest({
119
+ requestBody,
120
+ launch,
121
+ historyMessages: previous?.messages || fallbackAnchor
122
+ });
123
+ assertReasoningContinuationBudget(translated.messages, continuationState.attempts);
124
+ const idleTimeoutMs = resolveRequestTimeoutMs(launch);
125
+ const totalTimeoutMs = resolveTotalRequestTimeoutMs(launch, idleTimeoutMs);
126
+ logDebug('provider.bridge.request', {
127
+ traceId,
128
+ providerName: launch.providerName,
129
+ model: translated.body.model,
130
+ reasoningEffort: launch.reasoningEffort,
131
+ maxOutputTokens: translated.body.max_tokens,
132
+ stream: translated.body.stream,
133
+ hasPreviousResponseId: Boolean(previousResponseId),
134
+ historyHit: Boolean(previous),
135
+ usedFallbackAnchor: fallbackAnchor.length > 0,
136
+ inputItemCount: Array.isArray(requestBody.input) ? requestBody.input.length : requestBody.input ? 1 : 0,
137
+ messageCount: translated.messages.length,
138
+ continuationAttempts: continuationState.attempts,
139
+ idleTimeoutMs,
140
+ totalTimeoutMs
141
+ });
142
+ const controller = new AbortController();
143
+ activeRequests.add(controller);
144
+ 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
+ };
155
+ const onClientClose = () => {
156
+ if (!res.writableEnded) abortRequest('client_close');
157
+ };
158
+ req.on('aborted', onClientClose);
159
+ res.on('close', onClientClose);
160
+ refreshIdleTimeout();
161
+ const totalTimeout = setTimeout(() => abortRequest('total_timeout'), totalTimeoutMs);
162
+ try {
163
+ const upstream = await fetch(buildChatCompletionsUrl(launch.baseUrl, launch), {
164
+ method: 'POST',
165
+ headers: buildUpstreamHeaders(launch),
166
+ body: JSON.stringify(translated.body),
167
+ signal: controller.signal
168
+ });
169
+ refreshIdleTimeout();
170
+ if (!upstream.ok) {
171
+ await forwardUpstreamError(upstream, res, launch.apiKey);
172
+ return;
173
+ }
174
+ const context = {
175
+ launch,
176
+ requestBody,
177
+ model: translated.body.model,
178
+ toolKinds: translated.toolKinds
179
+ };
180
+ const remember = converted => {
181
+ const continuation = buildHistoryContinuation(converted);
182
+ const anchored = isReasoningContinuation(continuation);
183
+ const assistant = converted.assistantMessage || {};
184
+ const reasoningChars = typeof assistant.reasoning_content === 'string'
185
+ ? assistant.reasoning_content.length
186
+ : 0;
187
+ const contentChars = typeof assistant.content === 'string'
188
+ ? assistant.content.length
189
+ : 0;
190
+ const toolCallCount = Array.isArray(assistant.tool_calls)
191
+ ? assistant.tool_calls.length
192
+ : 0;
193
+ if (anchored) {
194
+ continuationState.anchorMessages = continuation;
195
+ continuationState.attempts += 1;
196
+ continuationState.responseId = converted.response.id;
197
+ } else {
198
+ continuationState.anchorMessages = [];
199
+ continuationState.attempts = 0;
200
+ continuationState.responseId = '';
201
+ }
202
+ logDebug('provider.bridge.response', {
203
+ traceId,
204
+ responseId: converted.response.id,
205
+ previousResponseId,
206
+ durationMs: Date.now() - startedAt,
207
+ providerFinishReason: converted.providerFinishReason || '',
208
+ responseStatus: converted.response.status,
209
+ incompleteReason: converted.response.incomplete_details?.reason || '',
210
+ reasoningChars,
211
+ contentChars,
212
+ toolCallCount,
213
+ continuationAnchored: anchored,
214
+ continuationAttempts: continuationState.attempts
215
+ });
216
+ rememberHistory(history, converted.response.id, [
217
+ ...translated.messages,
218
+ ...continuation
219
+ ]);
220
+ };
221
+ if (requestBody.stream !== false) {
222
+ const contentType = String(upstream.headers.get('content-type') || '').toLowerCase();
223
+ if (contentType.includes('text/event-stream')) {
224
+ await streamChatResponse({
225
+ upstream,
226
+ res,
227
+ context,
228
+ onComplete: remember,
229
+ onActivity: refreshIdleTimeout,
230
+ recoverInterrupted: () => (
231
+ ['idle_timeout', 'total_timeout'].includes(abortReason)
232
+ ? abortReason
233
+ : ''
234
+ )
235
+ });
236
+ } else {
237
+ const converted = convertChatResponse(await upstream.json(), context);
238
+ writeConvertedResponseAsSse(res, converted, context, remember);
239
+ }
240
+ } else {
241
+ const converted = convertChatResponse(await upstream.json(), context);
242
+ remember(converted);
243
+ sendJson(res, 200, converted.response);
244
+ }
245
+ } catch (error) {
246
+ logDebug('provider.bridge.error', {
247
+ traceId,
248
+ durationMs: Date.now() - startedAt,
249
+ code: normalizeBridgeErrorCode(error?.code),
250
+ aborted: controller.signal.aborted,
251
+ abortReason
252
+ });
253
+ if (controller.signal.aborted) {
254
+ throw providerError('provider_connection_timeout', 'The provider request was cancelled or timed out.');
255
+ }
256
+ throw error;
257
+ } finally {
258
+ clearTimeout(idleTimeout);
259
+ clearTimeout(totalTimeout);
260
+ activeRequests.delete(controller);
261
+ req.removeListener('aborted', onClientClose);
262
+ res.removeListener('close', onClientClose);
263
+ }
264
+ }
265
+
266
+ function buildChatCompletionsUrl(baseUrl, launch = {}) {
267
+ const url = new URL(baseUrl);
268
+ if (launch.fullEndpoint) {
269
+ for (const [key, value] of Object.entries(launch.queryParams || {})) {
270
+ url.searchParams.set(key, value);
271
+ }
272
+ return url.toString();
273
+ }
274
+ let pathname = url.pathname.replace(/\/+$/, '');
275
+ if (/\/chat\/completions$/i.test(pathname)) {
276
+ url.pathname = pathname;
277
+ for (const [key, value] of Object.entries(launch.queryParams || {})) {
278
+ url.searchParams.set(key, value);
279
+ }
280
+ return url.toString();
281
+ }
282
+ pathname = !pathname || pathname === '/'
283
+ ? '/v1'
284
+ : pathname;
285
+ url.pathname = `${pathname}/chat/completions`;
286
+ url.search = '';
287
+ url.hash = '';
288
+ for (const [key, value] of Object.entries(launch.queryParams || {})) {
289
+ url.searchParams.set(key, value);
290
+ }
291
+ return url.toString();
292
+ }
293
+
294
+ function buildUpstreamHeaders(launch = {}) {
295
+ const headers = {
296
+ accept: 'application/json, text/event-stream',
297
+ 'content-type': 'application/json',
298
+ 'user-agent': 'Codex-Overleaf-Link/provider-bridge',
299
+ ...(launch.customHeaders || {})
300
+ };
301
+ if (!launch.apiKey || launch.authMode === 'none') return headers;
302
+ if (launch.authMode === 'x-api-key') headers['x-api-key'] = launch.apiKey;
303
+ else if (launch.authMode === 'api-key') headers['api-key'] = launch.apiKey;
304
+ else if (launch.authMode === 'custom' && launch.apiKeyHeaderName) {
305
+ headers[launch.apiKeyHeaderName] = launch.apiKey;
306
+ } else {
307
+ headers.authorization = `Bearer ${launch.apiKey}`;
308
+ }
309
+ return headers;
310
+ }
311
+
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
+ function buildHistoryContinuation(converted = {}) {
327
+ const assistant = converted.assistantMessage || {};
328
+ const reasoning = typeof assistant.reasoning_content === 'string'
329
+ ? assistant.reasoning_content.trim()
330
+ : '';
331
+ const content = String(assistant.content || '').trim();
332
+ const toolCallCount = Array.isArray(assistant.tool_calls)
333
+ ? assistant.tool_calls.length
334
+ : 0;
335
+ // Some OpenAI-compatible DeepSeek gateways end a partial thinking stream
336
+ // with `finish_reason: stop`. A reasoning-only turn still has no usable
337
+ // answer or action for Codex, regardless of the provider's terminal label.
338
+ const reasoningOnlyTurn = reasoning && !content && toolCallCount === 0;
339
+ const interruptedContentTurn = converted.response?.status === 'incomplete'
340
+ && content
341
+ && toolCallCount === 0;
342
+ if (!reasoningOnlyTurn && !interruptedContentTurn) {
343
+ return [assistant];
344
+ }
345
+ const preservedOutput = interruptedContentTurn
346
+ ? `Partial answer preserved from the same unfinished task:\n\n${content}`
347
+ : `Partial analysis preserved from the same unfinished task:\n\n${reasoning}`;
348
+ return [
349
+ {
350
+ role: 'assistant',
351
+ content: preservedOutput
352
+ },
353
+ {
354
+ role: 'user',
355
+ content: [
356
+ REASONING_CONTINUATION_MARKER,
357
+ 'The preceding assistant message is established output from this same task.',
358
+ 'Continue from the exact point where that partial output stopped.',
359
+ 'Do not restart the analysis, reread files already covered, restate the task, or repeat conclusions above.',
360
+ 'Move to the next tool call or final answer as soon as the remaining reasoning is complete.'
361
+ ].join('\n')
362
+ }
363
+ ];
364
+ }
365
+
366
+ function isReasoningContinuation(messages = []) {
367
+ return messages.some(message => (
368
+ message?.role === 'user'
369
+ && typeof message.content === 'string'
370
+ && message.content.includes(REASONING_CONTINUATION_MARKER)
371
+ ));
372
+ }
373
+
374
+ function assertReasoningContinuationBudget(messages = [], stateAttempts = 0) {
375
+ const historyAttempts = messages.filter(message => (
376
+ message?.role === 'user'
377
+ && typeof message.content === 'string'
378
+ && message.content.includes(REASONING_CONTINUATION_MARKER)
379
+ )).length;
380
+ const count = Math.max(historyAttempts, Number(stateAttempts) || 0);
381
+ if (count <= MAX_REASONING_CONTINUATIONS) {
382
+ return;
383
+ }
384
+ throw providerError(
385
+ 'provider_reasoning_continuation_exhausted',
386
+ 'The provider repeatedly stopped during reasoning. Reduce the task scope or use a lower reasoning effort.'
387
+ );
388
+ }
389
+
390
+ function normalizeBridgeErrorCode(value) {
391
+ return [
392
+ 'provider_connection_timeout',
393
+ 'provider_response_invalid',
394
+ 'provider_agent_tools_incompatible',
395
+ 'provider_reasoning_continuation_exhausted',
396
+ 'provider_stream_tool_parse_failed',
397
+ 'provider_protocol_incompatible'
398
+ ].includes(value) ? value : 'provider_bridge_failed';
399
+ }
400
+
401
+ function bridgeErrorMessage(code) {
402
+ return code === 'provider_connection_timeout'
403
+ ? 'The provider request was cancelled or timed out.'
404
+ : 'The local provider bridge failed.';
405
+ }
406
+
407
+ async function forwardUpstreamError(upstream, res, apiKey) {
408
+ const text = (await upstream.text()).slice(0, MAX_ERROR_BYTES);
409
+ let message = `Provider returned HTTP ${upstream.status}.`;
410
+ try {
411
+ const parsed = JSON.parse(text);
412
+ message = parsed?.error?.message || parsed?.message || message;
413
+ } catch (_error) {}
414
+ sendJsonError(res, upstream.status, 'provider_upstream_error', sanitizeProviderMessage(message, [apiKey]));
415
+ }
416
+
417
+ function rememberHistory(history, responseId, messages) {
418
+ history.delete(responseId);
419
+ history.set(responseId, { messages });
420
+ while (history.size > MAX_HISTORY_ENTRIES) {
421
+ history.delete(history.keys().next().value);
422
+ }
423
+ }
424
+
425
+ function isAuthorized(req, token) {
426
+ const value = String(req.headers.authorization || '');
427
+ const supplied = value.startsWith('Bearer ') ? value.slice(7) : '';
428
+ const expectedBuffer = Buffer.from(token);
429
+ const suppliedBuffer = Buffer.from(supplied);
430
+ return expectedBuffer.length === suppliedBuffer.length
431
+ && crypto.timingSafeEqual(expectedBuffer, suppliedBuffer);
432
+ }
433
+
434
+ function readJsonBody(req) {
435
+ return new Promise((resolve, reject) => {
436
+ const chunks = [];
437
+ let bytes = 0;
438
+ req.on('data', chunk => {
439
+ bytes += chunk.length;
440
+ if (bytes > MAX_REQUEST_BYTES) {
441
+ reject(providerError('provider_request_too_large', 'Provider request exceeded the local bridge limit.'));
442
+ req.destroy();
443
+ return;
444
+ }
445
+ chunks.push(chunk);
446
+ });
447
+ req.on('end', () => {
448
+ try {
449
+ resolve(JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}'));
450
+ } catch (_error) {
451
+ reject(providerError('provider_request_invalid', 'Codex sent an invalid Responses request.'));
452
+ }
453
+ });
454
+ req.on('error', reject);
455
+ });
456
+ }
457
+
458
+ function sendJson(res, status, value) {
459
+ if (res.writableEnded) return;
460
+ res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
461
+ res.end(JSON.stringify(value));
462
+ }
463
+
464
+ function sendJsonError(res, status, code, message) {
465
+ sendJson(res, status, { error: { type: 'provider_error', code, message } });
466
+ }
467
+
468
+ function listen(server) {
469
+ return new Promise((resolve, reject) => {
470
+ server.once('error', reject);
471
+ server.listen(0, '127.0.0.1', () => {
472
+ server.removeListener('error', reject);
473
+ resolve();
474
+ });
475
+ });
476
+ }
477
+
478
+ function closeServer(server) {
479
+ return new Promise(resolve => {
480
+ server.close(() => resolve());
481
+ server.closeAllConnections?.();
482
+ });
483
+ }
484
+
485
+ module.exports = { buildChatCompletionsUrl, buildUpstreamHeaders, startChatCompletionsBridge };
@@ -0,0 +1,25 @@
1
+ 'use strict';
2
+
3
+ function resolveCodexCommand(env = process.env) {
4
+ if (
5
+ env.CODEX_OVERLEAF_ENV_READY === '1' ||
6
+ Object.prototype.hasOwnProperty.call(env, 'CODEX_OVERLEAF_CODEX_PATH')
7
+ ) {
8
+ return env.CODEX_OVERLEAF_CODEX_PATH || '';
9
+ }
10
+ return 'codex';
11
+ }
12
+
13
+ function shouldUseShellForCommand(command, env = process.env) {
14
+ const platform = env.CODEX_OVERLEAF_PLATFORM || process.platform;
15
+ if (platform !== 'win32') {
16
+ return false;
17
+ }
18
+ const text = String(command || '');
19
+ return text === 'codex' || /\.(?:cmd|bat)$/i.test(text);
20
+ }
21
+
22
+ module.exports = {
23
+ resolveCodexCommand,
24
+ shouldUseShellForCommand
25
+ };
@@ -35,6 +35,7 @@ function buildCodexTurnPrompt(options = {}) {
35
35
  '',
36
36
  'Project location citation rules:',
37
37
  formatProjectLocationCitationRules(),
38
+ formatMathOutputRules(),
38
39
  '',
39
40
  'Focused project file inventory:',
40
41
  formatFocusedProjectFileInventory(context.focusFiles),
@@ -263,6 +264,14 @@ function formatProjectLocationCitationRules() {
263
264
  ].join('\n');
264
265
  }
265
266
 
267
+ function formatMathOutputRules() {
268
+ return [
269
+ '- In user-visible prose, format inline mathematics as $...$ and display mathematics as $$...$$ on separate lines.',
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.'
272
+ ].join('\n');
273
+ }
274
+
266
275
  function formatFocusedProjectFileInventory(files) {
267
276
  const focusFiles = normalizeFocusFiles(files);
268
277
  if (!focusFiles.length) {