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.
- package/README.md +24 -23
- package/extension/bootstrap/manifest.template.json +2 -1
- package/extension/runtime-manifest.json +12 -6
- package/extension/src/background.js +7 -0
- package/extension/src/content/composerPanel.js +13 -3
- package/extension/src/content/contentRuntime.js +129 -64
- package/extension/src/content/contextTray.js +173 -9
- package/extension/src/content/markdownText.js +16 -1
- package/extension/src/content/mathText.js +233 -0
- package/extension/src/content/modelPicker.js +117 -106
- package/extension/src/content/modelPickerSupport.js +115 -0
- package/extension/src/content/nativeChannel.js +20 -1
- package/extension/src/content/panelMaintenance.js +2 -0
- package/extension/src/content/projectSettingsCoordinator.js +2 -0
- package/extension/src/content/providerSettingsCoordinator.js +413 -0
- package/extension/src/content/providerSettingsDialog.js +1006 -0
- package/extension/src/content/runTimelineView.js +24 -5
- package/extension/src/content/settingsPanel.js +33 -2
- package/extension/src/content/writebackOrchestrator.js +3 -1
- package/extension/src/page/overleafProjectSnapshot.js +29 -33
- package/extension/src/page/snapshotRouter.js +75 -8
- package/extension/src/page/writebackRouter.js +20 -33
- package/extension/src/pageBridge.js +1 -1
- package/extension/src/shared/agentTranscript.js +130 -0
- package/extension/src/shared/compatibility.js +3 -2
- package/extension/src/shared/failureReasons.js +75 -0
- package/extension/src/shared/i18n.js +22 -0
- package/extension/src/shared/models.js +1 -0
- package/extension/src/shared/providerProfiles.js +283 -0
- package/extension/src/shared/sessionState.js +12 -2
- package/extension/src/shared/storageDb.js +1 -1
- package/extension/styles/panel.css +689 -1
- package/extension/vendor/katex/LICENSE +21 -0
- package/extension/vendor/katex/VERSION +2 -0
- package/extension/vendor/katex/fonts/KaTeX_AMS-Regular.ttf +0 -0
- package/extension/vendor/katex/fonts/KaTeX_AMS-Regular.woff +0 -0
- package/extension/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Main-Bold.ttf +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Main-Bold.woff +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Main-Italic.ttf +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Main-Italic.woff +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Main-Regular.ttf +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Main-Regular.woff +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Math-Italic.ttf +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Math-Italic.woff +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- package/extension/vendor/katex/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
- package/extension/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff +0 -0
- package/extension/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/extension/vendor/katex/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
- package/extension/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff +0 -0
- package/extension/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/extension/vendor/katex/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
- package/extension/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff +0 -0
- package/extension/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Script-Regular.ttf +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Script-Regular.woff +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Size1-Regular.ttf +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Size1-Regular.woff +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Size2-Regular.ttf +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Size2-Regular.woff +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Size3-Regular.ttf +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Size3-Regular.woff +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Size4-Regular.ttf +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Size4-Regular.woff +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff +0 -0
- package/extension/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/extension/vendor/katex/katex.min.css +1 -0
- package/extension/vendor/katex/katex.min.js +1 -0
- package/native-host/src/anthropicBridgeRequest.js +435 -0
- package/native-host/src/anthropicBridgeResponse.js +431 -0
- package/native-host/src/anthropicMessagesBridge.js +281 -0
- package/native-host/src/chatBridgeRequest.js +274 -0
- package/native-host/src/chatBridgeResponse.js +594 -0
- package/native-host/src/chatCompletionsBridge.js +485 -0
- package/native-host/src/codexCommand.js +25 -0
- package/native-host/src/codexPromptAssembly.js +9 -0
- package/native-host/src/codexProviderLaunch.js +213 -0
- package/native-host/src/codexProviderTest.js +196 -0
- package/native-host/src/codexSessionRunner.js +29 -25
- package/native-host/src/managedInstall.js +1 -0
- package/native-host/src/providerBridgeRoutes.js +11 -0
- package/native-host/src/providerModelCatalog.js +107 -0
- package/native-host/src/providerProfile.js +454 -0
- package/native-host/src/providerReasoning.js +235 -0
- package/native-host/src/providerRedaction.js +18 -0
- package/native-host/src/providerRuntime.js +365 -0
- package/native-host/src/providerStore.js +691 -0
- package/native-host/src/readProgressGuard.js +360 -0
- package/native-host/src/taskRunnerRuntime.js +39 -3
- package/native-host/src/updateArchive.js +1 -0
- 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) {
|