codex-overleaf-link 2.1.4 → 2.2.0
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 +27 -23
- package/extension/bootstrap/background.js +14 -1
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/runtime-manifest.json +11 -0
- package/extension/src/background.js +507 -8
- package/extension/src/backgroundUpdateCoordinator.js +284 -19
- package/extension/src/content/activeTurnControl.js +179 -0
- package/extension/src/content/composerPanel.js +19 -6
- package/extension/src/content/contentRuntime.js +335 -317
- package/extension/src/content/diagnosticsPanel.js +132 -1
- package/extension/src/content/markdownDomRenderer.js +451 -0
- package/extension/src/content/markdownText.js +86 -7
- package/extension/src/content/mathText.js +49 -4
- package/extension/src/content/modelPicker.js +14 -5
- package/extension/src/content/nativeChannel.js +7 -1
- package/extension/src/content/otWarmMirror.js +1 -1
- package/extension/src/content/panelRenderer.js +112 -12
- package/extension/src/content/pendingInputView.js +106 -0
- package/extension/src/content/projectProviderSelection.js +68 -0
- package/extension/src/content/projectSessionCleanup.js +104 -0
- package/extension/src/content/providerSettingsCoordinator.js +170 -22
- package/extension/src/content/providerSettingsDialog.js +2 -2
- package/extension/src/content/recentProjects.js +24 -13
- package/extension/src/content/runController.js +2 -2
- package/extension/src/content/runGuidanceController.js +70 -0
- package/extension/src/content/runGuidanceView.js +48 -0
- package/extension/src/content/runQueueScheduler.js +260 -0
- package/extension/src/content/runTimelineView.js +33 -80
- package/extension/src/content/sessionManager.js +2 -2
- package/extension/src/content/sessionPersistence.js +62 -0
- package/extension/src/content/settingsPanel.js +1 -1
- package/extension/src/content/updateNotice.js +80 -1
- package/extension/src/page/writebackRouter.js +3 -9
- package/extension/src/shared/compatibility.js +2 -1
- package/extension/src/shared/i18n.js +54 -0
- package/extension/src/shared/providerProfiles.js +3 -2
- package/extension/src/shared/runInputQueue.js +253 -0
- package/extension/src/shared/sessionState.js +45 -22
- package/extension/src/shared/storageDb.js +39 -44
- package/extension/src/shared/storageMigration.js +123 -3
- package/extension/src/shared/storageRunActions.js +34 -1
- package/extension/src/shared/updateConsent.js +15 -4
- package/extension/styles/panel.css +370 -23
- package/extension/vendor/katex/katex.min.css +1 -1
- package/extension/vendor/markdown-it/LICENSE +22 -0
- package/extension/vendor/markdown-it/SOURCE.json +7 -0
- package/extension/vendor/markdown-it/VERSION +1 -0
- package/extension/vendor/markdown-it/markdown-it.min.js +3 -0
- package/native-host/src/anthropicBridgeRequest.js +3 -3
- package/native-host/src/anthropicBridgeResponse.js +58 -17
- package/native-host/src/anthropicMessagesBridge.js +156 -23
- package/native-host/src/chatBridgeRequest.js +1 -1
- package/native-host/src/chatBridgeResponse.js +12 -8
- package/native-host/src/chatCompletionsBridge.js +22 -40
- package/native-host/src/codexPromptAssembly.js +3 -1
- package/native-host/src/codexProviderLaunch.js +6 -3
- package/native-host/src/codexSessionRunner.js +66 -66
- package/native-host/src/codexSessionTiming.js +45 -0
- package/native-host/src/diffEngine.js +10 -5
- package/native-host/src/index.js +58 -3
- package/native-host/src/managedInstall.js +45 -7
- package/native-host/src/providerProfile.js +2 -2
- package/native-host/src/providerStreamLifecycle.js +100 -0
- package/native-host/src/responsesPassthroughBridge.js +636 -0
- package/native-host/src/taskRunnerRuntime.js +81 -2
- package/native-host/src/updateArchive.js +4 -3
- package/native-host/src/updateManager.js +306 -19
- package/native-host/src/updateTrust.js +1 -1
- package/package.json +3 -1
|
@@ -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
|
|
53
|
+
const idleTimeoutMs = resolveProviderIdleTimeoutMs(launch);
|
|
54
|
+
const totalTimeoutMs = resolveProviderTotalTimeoutMs(launch, idleTimeoutMs);
|
|
44
55
|
server.keepAliveTimeout = 5000;
|
|
45
|
-
server.requestTimeout =
|
|
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
|
|
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)
|
|
127
|
+
if (!res.writableEnded) lifecycle.abort('client_close');
|
|
89
128
|
};
|
|
90
129
|
req.on('aborted', onClientClose);
|
|
91
130
|
res.on('close', onClientClose);
|
|
92
|
-
|
|
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 =>
|
|
106
|
-
|
|
107
|
-
|
|
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({
|
|
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({
|
|
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)
|
|
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
|
-
|
|
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
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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 [
|
|
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
|
-
|
|
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:
|
|
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'
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
65
|
+
const requestTimeoutMs = resolveProviderIdleTimeoutMs(launch);
|
|
66
|
+
const totalTimeoutMs = resolveProviderTotalTimeoutMs(launch, requestTimeoutMs);
|
|
61
67
|
server.keepAliveTimeout = 5000;
|
|
62
|
-
server.requestTimeout =
|
|
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 =
|
|
125
|
-
const totalTimeoutMs =
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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)
|
|
158
|
+
if (!res.writableEnded) lifecycle.abort('client_close');
|
|
157
159
|
};
|
|
158
160
|
req.on('aborted', onClientClose);
|
|
159
161
|
res.on('close', onClientClose);
|
|
160
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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 ||
|
|
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
|
-
:
|
|
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,
|