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
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const http = require('node:http');
|
|
5
|
+
const { classifyResponsesRoute } = require('./providerBridgeRoutes');
|
|
6
|
+
const { providerError } = require('./providerProfile');
|
|
7
|
+
const { sanitizeProviderMessage } = require('./providerRedaction');
|
|
8
|
+
const { logDebug } = require('./debugLog');
|
|
9
|
+
const {
|
|
10
|
+
createProviderStreamLifecycle,
|
|
11
|
+
resolveProviderIdleTimeoutMs,
|
|
12
|
+
resolveProviderTotalTimeoutMs
|
|
13
|
+
} = require('./providerStreamLifecycle');
|
|
14
|
+
|
|
15
|
+
const MAX_REQUEST_BYTES = 16 * 1024 * 1024;
|
|
16
|
+
const MAX_ERROR_BYTES = 64 * 1024;
|
|
17
|
+
const MAX_HISTORY_ENTRIES = 64;
|
|
18
|
+
const MAX_HISTORY_ITEMS = 4096;
|
|
19
|
+
const MAX_HISTORY_BYTES = 32 * 1024 * 1024;
|
|
20
|
+
const MAX_DUPLICATE_CONTINUATIONS = 2;
|
|
21
|
+
const MAX_STREAM_BUFFER_BYTES = 8 * 1024 * 1024;
|
|
22
|
+
const DEFAULT_MAX_OUTPUT_TOKENS = 65536;
|
|
23
|
+
const CONTINUATION_MARKER = '[codex-overleaf-provider-reasoning-continuation]';
|
|
24
|
+
|
|
25
|
+
async function startResponsesPassthroughBridge({ launch, signal } = {}) {
|
|
26
|
+
if (!launch?.baseUrl) {
|
|
27
|
+
throw providerError('provider_base_url_invalid', 'Responses routing requires a provider Base URL.');
|
|
28
|
+
}
|
|
29
|
+
const clientToken = crypto.randomBytes(32).toString('base64url');
|
|
30
|
+
const activeRequests = new Set();
|
|
31
|
+
const history = new Map();
|
|
32
|
+
const server = http.createServer((req, res) => {
|
|
33
|
+
handleRequest({ req, res, launch, clientToken, activeRequests, history }).catch(error => {
|
|
34
|
+
if (res.writableEnded) return;
|
|
35
|
+
const code = normalizeBridgeErrorCode(error?.code);
|
|
36
|
+
const message = sanitizeProviderMessage(error?.message, [launch.apiKey]) || bridgeErrorMessage(code);
|
|
37
|
+
if (res.headersSent) {
|
|
38
|
+
const payload = {
|
|
39
|
+
type: 'response.failed',
|
|
40
|
+
sequence_number: 999999,
|
|
41
|
+
response: { status: 'failed', error: { code, message } }
|
|
42
|
+
};
|
|
43
|
+
res.end(`event: response.failed\ndata: ${JSON.stringify(payload)}\n\n`);
|
|
44
|
+
} else {
|
|
45
|
+
sendJsonError(res, code === 'provider_connection_timeout' ? 504 : 502, code, message);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
const idleTimeoutMs = resolveProviderIdleTimeoutMs(launch);
|
|
50
|
+
const totalTimeoutMs = resolveProviderTotalTimeoutMs(launch, idleTimeoutMs);
|
|
51
|
+
server.keepAliveTimeout = 5000;
|
|
52
|
+
server.requestTimeout = totalTimeoutMs + 5000;
|
|
53
|
+
await listen(server);
|
|
54
|
+
const address = server.address();
|
|
55
|
+
const baseUrl = `http://127.0.0.1:${address.port}/v1`;
|
|
56
|
+
let closed = false;
|
|
57
|
+
const close = async () => {
|
|
58
|
+
if (closed) return;
|
|
59
|
+
closed = true;
|
|
60
|
+
signal?.removeEventListener('abort', close);
|
|
61
|
+
for (const controller of activeRequests) controller.abort();
|
|
62
|
+
activeRequests.clear();
|
|
63
|
+
history.clear();
|
|
64
|
+
await closeServer(server);
|
|
65
|
+
};
|
|
66
|
+
signal?.addEventListener('abort', close, { once: true });
|
|
67
|
+
if (signal?.aborted) await close();
|
|
68
|
+
return { baseUrl, clientToken, close };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function handleRequest({ req, res, launch, clientToken, activeRequests, history }) {
|
|
72
|
+
if (!isAuthorized(req, clientToken)) {
|
|
73
|
+
sendJsonError(res, 401, 'unauthorized', 'Local bridge authorization failed.');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const url = new URL(req.url || '/', 'http://127.0.0.1');
|
|
77
|
+
if (req.method === 'GET' && /\/models\/?$/.test(url.pathname)) {
|
|
78
|
+
sendJson(res, 200, {
|
|
79
|
+
object: 'list',
|
|
80
|
+
data: [{ id: launch.modelId, object: 'model', created: 0, owned_by: launch.providerName || 'custom' }]
|
|
81
|
+
});
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const route = classifyResponsesRoute(req.method, url.pathname);
|
|
85
|
+
if (!route) {
|
|
86
|
+
sendJsonError(res, 404, 'not_found', 'The local provider bridge only exposes Responses, Responses Compact, and Models endpoints.');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const requestBody = await readJsonBody(req);
|
|
91
|
+
const traceId = crypto.randomUUID().slice(0, 12);
|
|
92
|
+
const startedAt = Date.now();
|
|
93
|
+
const originalPreviousResponseId = String(requestBody.previous_response_id || '');
|
|
94
|
+
const previous = history.get(originalPreviousResponseId);
|
|
95
|
+
if (previous?.duplicateContinuations >= MAX_DUPLICATE_CONTINUATIONS) {
|
|
96
|
+
throw providerError(
|
|
97
|
+
'provider_responses_continuation_repeated',
|
|
98
|
+
'The provider repeated the same unfinished response without making progress.'
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const prepared = route === 'responses'
|
|
103
|
+
? prepareResponsesRequest({ requestBody, launch, history, previous, originalPreviousResponseId })
|
|
104
|
+
: {
|
|
105
|
+
body: { ...requestBody },
|
|
106
|
+
currentInput: normalizeInputItems(requestBody.input),
|
|
107
|
+
replayed: false
|
|
108
|
+
};
|
|
109
|
+
const idleTimeoutMs = resolveProviderIdleTimeoutMs(launch);
|
|
110
|
+
const totalTimeoutMs = resolveProviderTotalTimeoutMs(launch, idleTimeoutMs);
|
|
111
|
+
logDebug('provider.responses.request', {
|
|
112
|
+
traceId,
|
|
113
|
+
providerName: launch.providerName,
|
|
114
|
+
model: prepared.body.model || launch.modelId,
|
|
115
|
+
route,
|
|
116
|
+
reasoningEffort: launch.reasoningEffort,
|
|
117
|
+
maxOutputTokens: route === 'responses' ? prepared.body.max_output_tokens : null,
|
|
118
|
+
stream: prepared.body.stream !== false,
|
|
119
|
+
hasPreviousResponseId: Boolean(originalPreviousResponseId),
|
|
120
|
+
historyHit: Boolean(previous),
|
|
121
|
+
replayed: prepared.replayed,
|
|
122
|
+
inputItemCount: prepared.currentInput.length,
|
|
123
|
+
idleTimeoutMs,
|
|
124
|
+
totalTimeoutMs
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const controller = new AbortController();
|
|
128
|
+
activeRequests.add(controller);
|
|
129
|
+
let abortReason = '';
|
|
130
|
+
const lifecycle = createProviderStreamLifecycle({
|
|
131
|
+
controller,
|
|
132
|
+
idleTimeoutMs,
|
|
133
|
+
totalTimeoutMs,
|
|
134
|
+
onAbort: reason => { abortReason = reason; }
|
|
135
|
+
});
|
|
136
|
+
const onClientClose = () => {
|
|
137
|
+
if (!res.writableEnded) lifecycle.abort('client_close');
|
|
138
|
+
};
|
|
139
|
+
req.on('aborted', onClientClose);
|
|
140
|
+
res.on('close', onClientClose);
|
|
141
|
+
lifecycle.start();
|
|
142
|
+
try {
|
|
143
|
+
const upstream = await fetch(buildResponsesUrl(launch.baseUrl, launch, route), {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
headers: buildUpstreamHeaders(launch),
|
|
146
|
+
body: JSON.stringify(prepared.body),
|
|
147
|
+
signal: controller.signal
|
|
148
|
+
});
|
|
149
|
+
lifecycle.touch();
|
|
150
|
+
if (!upstream.ok) {
|
|
151
|
+
await forwardUpstreamError(upstream, res, launch.apiKey);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (route === 'compact') {
|
|
155
|
+
await forwardOpaqueResponse(upstream, res, lifecycle.touch);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let response;
|
|
160
|
+
if (prepared.body.stream !== false) {
|
|
161
|
+
const contentType = String(upstream.headers.get('content-type') || '').toLowerCase();
|
|
162
|
+
if (!contentType.includes('text/event-stream')) {
|
|
163
|
+
throw providerError(
|
|
164
|
+
'provider_protocol_incompatible',
|
|
165
|
+
'The provider returned a non-streaming payload for a streaming Responses request.'
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
response = await forwardResponsesSse(upstream, res, lifecycle.touch);
|
|
169
|
+
} else {
|
|
170
|
+
response = await upstream.json();
|
|
171
|
+
lifecycle.touch();
|
|
172
|
+
sendJson(res, 200, response);
|
|
173
|
+
}
|
|
174
|
+
if (response?.id) {
|
|
175
|
+
rememberResponse(history, {
|
|
176
|
+
response,
|
|
177
|
+
previous,
|
|
178
|
+
previousResponseId: originalPreviousResponseId,
|
|
179
|
+
currentInput: prepared.currentInput,
|
|
180
|
+
replayed: prepared.replayed
|
|
181
|
+
});
|
|
182
|
+
const entry = history.get(String(response.id));
|
|
183
|
+
logDebug('provider.responses.response', {
|
|
184
|
+
traceId,
|
|
185
|
+
responseId: response.id,
|
|
186
|
+
previousResponseId: originalPreviousResponseId,
|
|
187
|
+
durationMs: Date.now() - startedAt,
|
|
188
|
+
responseStatus: response.status || '',
|
|
189
|
+
incompleteReason: response.incomplete_details?.reason || '',
|
|
190
|
+
outputItemCount: entry?.outputItems.length || 0,
|
|
191
|
+
reasoningChars: entry?.metrics.reasoningChars || 0,
|
|
192
|
+
contentChars: entry?.metrics.contentChars || 0,
|
|
193
|
+
toolCallCount: entry?.metrics.toolCallCount || 0,
|
|
194
|
+
continuationRequired: Boolean(entry?.continuationRequired),
|
|
195
|
+
duplicateContinuations: entry?.duplicateContinuations || 0,
|
|
196
|
+
replayed: prepared.replayed,
|
|
197
|
+
outputTokens: Number(response.usage?.output_tokens) || 0
|
|
198
|
+
});
|
|
199
|
+
} else {
|
|
200
|
+
logDebug('provider.responses.response', {
|
|
201
|
+
traceId,
|
|
202
|
+
durationMs: Date.now() - startedAt,
|
|
203
|
+
responseStatus: 'missing_terminal_response',
|
|
204
|
+
replayed: prepared.replayed
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
} catch (error) {
|
|
208
|
+
logDebug('provider.responses.error', {
|
|
209
|
+
traceId,
|
|
210
|
+
durationMs: Date.now() - startedAt,
|
|
211
|
+
code: normalizeBridgeErrorCode(error?.code),
|
|
212
|
+
aborted: controller.signal.aborted,
|
|
213
|
+
abortReason,
|
|
214
|
+
replayed: prepared.replayed
|
|
215
|
+
});
|
|
216
|
+
if (controller.signal.aborted) {
|
|
217
|
+
throw providerError('provider_connection_timeout', 'The provider request was cancelled or timed out.');
|
|
218
|
+
}
|
|
219
|
+
throw error;
|
|
220
|
+
} finally {
|
|
221
|
+
lifecycle.dispose();
|
|
222
|
+
activeRequests.delete(controller);
|
|
223
|
+
req.removeListener('aborted', onClientClose);
|
|
224
|
+
res.removeListener('close', onClientClose);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function prepareResponsesRequest({ requestBody, launch, history, previous, originalPreviousResponseId }) {
|
|
229
|
+
const currentInput = normalizeInputItems(requestBody.input);
|
|
230
|
+
const body = {
|
|
231
|
+
...requestBody,
|
|
232
|
+
max_output_tokens: resolveMaxOutputTokens(launch.maxOutputTokens, requestBody.max_output_tokens)
|
|
233
|
+
};
|
|
234
|
+
if (!previous?.continuationRequired) {
|
|
235
|
+
return { body, currentInput, replayed: false };
|
|
236
|
+
}
|
|
237
|
+
if (previous.replayUnavailableReason) {
|
|
238
|
+
throw providerError('provider_responses_continuation_unrecoverable', previous.replayUnavailableReason);
|
|
239
|
+
}
|
|
240
|
+
const replayInput = buildReplayInput(history, originalPreviousResponseId);
|
|
241
|
+
const instruction = buildContinuationInstruction();
|
|
242
|
+
body.input = [...replayInput, ...currentInput, instruction];
|
|
243
|
+
delete body.previous_response_id;
|
|
244
|
+
logDebug('provider.responses.replay', {
|
|
245
|
+
providerName: launch.providerName,
|
|
246
|
+
model: body.model || launch.modelId,
|
|
247
|
+
previousResponseId: originalPreviousResponseId,
|
|
248
|
+
replayItemCount: replayInput.length,
|
|
249
|
+
currentInputItemCount: currentInput.length
|
|
250
|
+
});
|
|
251
|
+
return { body, currentInput: [...currentInput, instruction], replayed: true };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function rememberResponse(history, { response, previous, previousResponseId, currentInput, replayed }) {
|
|
255
|
+
const responseId = String(response.id || '');
|
|
256
|
+
if (!responseId) return;
|
|
257
|
+
const outputItems = Array.isArray(response.output) ? response.output : [];
|
|
258
|
+
const metrics = inspectOutput(response, outputItems);
|
|
259
|
+
const incompleteReason = String(response.incomplete_details?.reason || '');
|
|
260
|
+
const continuationRequired = ['max_output_tokens', 'max_tokens', 'length'].includes(incompleteReason)
|
|
261
|
+
|| (response.status !== 'incomplete' && metrics.hasReasoning && metrics.contentChars === 0 && metrics.toolCallCount === 0);
|
|
262
|
+
const duplicate = replayed && previous
|
|
263
|
+
? outputsRepeat(previous.metrics.normalizedOutput, metrics.normalizedOutput)
|
|
264
|
+
: false;
|
|
265
|
+
const duplicateContinuations = duplicate
|
|
266
|
+
? (Number(previous?.duplicateContinuations) || 0) + 1
|
|
267
|
+
: 0;
|
|
268
|
+
const serializedBytes = serializedSize(currentInput) + serializedSize(outputItems);
|
|
269
|
+
const replayUnavailableReason = outputItems.length > MAX_HISTORY_ITEMS || serializedBytes > MAX_HISTORY_BYTES
|
|
270
|
+
? 'The unfinished provider response is too large to replay safely.'
|
|
271
|
+
: '';
|
|
272
|
+
history.delete(responseId);
|
|
273
|
+
history.set(responseId, {
|
|
274
|
+
responseId,
|
|
275
|
+
previousResponseId,
|
|
276
|
+
requestInput: currentInput,
|
|
277
|
+
outputItems,
|
|
278
|
+
status: String(response.status || ''),
|
|
279
|
+
incompleteReason: String(response.incomplete_details?.reason || ''),
|
|
280
|
+
continuationRequired,
|
|
281
|
+
duplicateContinuations,
|
|
282
|
+
replayUnavailableReason,
|
|
283
|
+
metrics,
|
|
284
|
+
serializedBytes,
|
|
285
|
+
createdAt: Date.now()
|
|
286
|
+
});
|
|
287
|
+
pruneHistory(history);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function buildReplayInput(history, responseId) {
|
|
291
|
+
const chain = [];
|
|
292
|
+
const visited = new Set();
|
|
293
|
+
let currentId = String(responseId || '');
|
|
294
|
+
while (currentId) {
|
|
295
|
+
if (visited.has(currentId)) {
|
|
296
|
+
throw providerError('provider_responses_continuation_unrecoverable', 'The provider response lineage contains a cycle.');
|
|
297
|
+
}
|
|
298
|
+
visited.add(currentId);
|
|
299
|
+
const entry = history.get(currentId);
|
|
300
|
+
if (!entry) {
|
|
301
|
+
throw providerError(
|
|
302
|
+
'provider_responses_continuation_unrecoverable',
|
|
303
|
+
'The unfinished provider response no longer has enough local history to continue safely.'
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
chain.unshift(entry);
|
|
307
|
+
currentId = String(entry.previousResponseId || '');
|
|
308
|
+
if (chain.length > MAX_HISTORY_ENTRIES) {
|
|
309
|
+
throw providerError('provider_continuation_history_limit', 'The provider continuation history is too long to replay safely.');
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const items = [];
|
|
313
|
+
let bytes = 0;
|
|
314
|
+
for (const entry of chain) {
|
|
315
|
+
items.push(...entry.requestInput, ...entry.outputItems);
|
|
316
|
+
bytes += entry.serializedBytes;
|
|
317
|
+
if (items.length > MAX_HISTORY_ITEMS || bytes > MAX_HISTORY_BYTES) {
|
|
318
|
+
throw providerError('provider_continuation_history_limit', 'The provider continuation history is too large to replay safely.');
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return items;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function inspectOutput(response, outputItems) {
|
|
325
|
+
const reasoningParts = [];
|
|
326
|
+
const contentParts = [];
|
|
327
|
+
const toolParts = [];
|
|
328
|
+
let hasReasoning = false;
|
|
329
|
+
for (const item of outputItems) {
|
|
330
|
+
const type = String(item?.type || '');
|
|
331
|
+
if (type === 'reasoning') {
|
|
332
|
+
hasReasoning = true;
|
|
333
|
+
collectTextParts(item.summary, reasoningParts);
|
|
334
|
+
collectTextParts(item.content, reasoningParts);
|
|
335
|
+
} else if (type === 'message') {
|
|
336
|
+
collectTextParts(item.content, contentParts);
|
|
337
|
+
} else if (type === 'function_call' || type === 'custom_tool_call' || type === 'computer_call') {
|
|
338
|
+
toolParts.push([type, item.name, item.call_id, item.arguments, item.input].filter(Boolean).join(':'));
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (typeof response.output_text === 'string') contentParts.push(response.output_text);
|
|
342
|
+
const reasoningText = reasoningParts.join('\n');
|
|
343
|
+
const contentText = contentParts.join('\n');
|
|
344
|
+
const normalizedOutput = normalizeComparableText([reasoningText, contentText, ...toolParts].join('\n'));
|
|
345
|
+
return {
|
|
346
|
+
hasReasoning,
|
|
347
|
+
reasoningChars: reasoningText.length,
|
|
348
|
+
contentChars: contentText.length,
|
|
349
|
+
toolCallCount: toolParts.length,
|
|
350
|
+
normalizedOutput,
|
|
351
|
+
outputFingerprint: normalizedOutput
|
|
352
|
+
? crypto.createHash('sha256').update(normalizedOutput).digest('hex').slice(0, 16)
|
|
353
|
+
: ''
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function collectTextParts(value, target) {
|
|
358
|
+
if (!Array.isArray(value)) return;
|
|
359
|
+
for (const part of value) {
|
|
360
|
+
const text = typeof part === 'string' ? part : part?.text;
|
|
361
|
+
if (typeof text === 'string' && text) target.push(text);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function outputsRepeat(previous, current) {
|
|
366
|
+
if (!previous || !current) return false;
|
|
367
|
+
return previous === current;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function normalizeComparableText(value) {
|
|
371
|
+
return String(value || '')
|
|
372
|
+
.normalize('NFKC')
|
|
373
|
+
.toLowerCase()
|
|
374
|
+
.replace(/\s+/g, ' ')
|
|
375
|
+
.trim();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function normalizeInputItems(value) {
|
|
379
|
+
if (Array.isArray(value)) return value.slice();
|
|
380
|
+
if (typeof value === 'string') {
|
|
381
|
+
return [{ role: 'user', content: [{ type: 'input_text', text: value }] }];
|
|
382
|
+
}
|
|
383
|
+
return value && typeof value === 'object' ? [value] : [];
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function buildContinuationInstruction() {
|
|
387
|
+
return {
|
|
388
|
+
role: 'user',
|
|
389
|
+
content: [{
|
|
390
|
+
type: 'input_text',
|
|
391
|
+
text: [
|
|
392
|
+
CONTINUATION_MARKER,
|
|
393
|
+
'The preceding response output is established work from this same unfinished task.',
|
|
394
|
+
'Continue from the exact point where it stopped.',
|
|
395
|
+
'Do not restart the analysis, reread files already covered, or repeat conclusions already produced.',
|
|
396
|
+
'Proceed to the next tool call or final answer as soon as the remaining reasoning is complete.'
|
|
397
|
+
].join('\n')
|
|
398
|
+
}]
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function resolveMaxOutputTokens(configured, requested) {
|
|
403
|
+
const value = Number(configured ?? requested ?? DEFAULT_MAX_OUTPUT_TOKENS);
|
|
404
|
+
if (!Number.isFinite(value) || value <= 0) return DEFAULT_MAX_OUTPUT_TOKENS;
|
|
405
|
+
return Math.max(1, Math.floor(value));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function forwardResponsesSse(upstream, res, onActivity) {
|
|
409
|
+
res.writeHead(200, {
|
|
410
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
411
|
+
'cache-control': 'no-cache, no-store',
|
|
412
|
+
connection: 'keep-alive'
|
|
413
|
+
});
|
|
414
|
+
if (!upstream.body) {
|
|
415
|
+
throw providerError('provider_response_invalid', 'The provider returned an empty Responses stream.');
|
|
416
|
+
}
|
|
417
|
+
const reader = upstream.body.getReader();
|
|
418
|
+
const decoder = new TextDecoder();
|
|
419
|
+
const observer = { buffer: '', terminalResponse: null };
|
|
420
|
+
while (true) {
|
|
421
|
+
const { done, value } = await reader.read();
|
|
422
|
+
if (done) break;
|
|
423
|
+
onActivity();
|
|
424
|
+
observeSseText(observer, decoder.decode(value, { stream: true }));
|
|
425
|
+
if (!res.writableEnded) res.write(Buffer.from(value));
|
|
426
|
+
}
|
|
427
|
+
observeSseText(observer, decoder.decode(), true);
|
|
428
|
+
if (!res.writableEnded) res.end();
|
|
429
|
+
return observer.terminalResponse;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function observeSseText(observer, text, flush = false) {
|
|
433
|
+
observer.buffer += text;
|
|
434
|
+
if (Buffer.byteLength(observer.buffer, 'utf8') > MAX_STREAM_BUFFER_BYTES) {
|
|
435
|
+
throw providerError('provider_stream_frame_too_large', 'Provider stream frame exceeded the local bridge limit.');
|
|
436
|
+
}
|
|
437
|
+
let match;
|
|
438
|
+
while ((match = /\r?\n\r?\n/.exec(observer.buffer))) {
|
|
439
|
+
const block = observer.buffer.slice(0, match.index);
|
|
440
|
+
observer.buffer = observer.buffer.slice(match.index + match[0].length);
|
|
441
|
+
observeSseBlock(observer, block);
|
|
442
|
+
}
|
|
443
|
+
if (flush && observer.buffer.trim()) {
|
|
444
|
+
observeSseBlock(observer, observer.buffer);
|
|
445
|
+
observer.buffer = '';
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function observeSseBlock(observer, block) {
|
|
450
|
+
const data = String(block || '')
|
|
451
|
+
.split(/\r?\n/)
|
|
452
|
+
.filter(line => line.startsWith('data:'))
|
|
453
|
+
.map(line => line.slice(5).trimStart())
|
|
454
|
+
.join('\n');
|
|
455
|
+
if (!data || data === '[DONE]') return;
|
|
456
|
+
try {
|
|
457
|
+
const payload = JSON.parse(data);
|
|
458
|
+
if (['response.completed', 'response.incomplete', 'response.failed'].includes(payload?.type) && payload.response) {
|
|
459
|
+
observer.terminalResponse = payload.response;
|
|
460
|
+
}
|
|
461
|
+
} catch (_error) {}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async function forwardOpaqueResponse(upstream, res, onActivity) {
|
|
465
|
+
const contentType = upstream.headers.get('content-type') || 'application/json; charset=utf-8';
|
|
466
|
+
res.writeHead(upstream.status, { 'content-type': contentType, 'cache-control': 'no-store' });
|
|
467
|
+
if (!upstream.body) {
|
|
468
|
+
res.end();
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const reader = upstream.body.getReader();
|
|
472
|
+
while (true) {
|
|
473
|
+
const { done, value } = await reader.read();
|
|
474
|
+
if (done) break;
|
|
475
|
+
onActivity();
|
|
476
|
+
if (!res.writableEnded) res.write(Buffer.from(value));
|
|
477
|
+
}
|
|
478
|
+
if (!res.writableEnded) res.end();
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function buildResponsesUrl(baseUrl, launch = {}, route = 'responses') {
|
|
482
|
+
const url = new URL(baseUrl);
|
|
483
|
+
let pathname = url.pathname.replace(/\/+$/, '');
|
|
484
|
+
if (launch.fullEndpoint) {
|
|
485
|
+
if (route === 'compact' && /\/responses$/i.test(pathname)) {
|
|
486
|
+
url.pathname = `${pathname}/compact`;
|
|
487
|
+
}
|
|
488
|
+
} else if (/\/responses(?:\/compact)?$/i.test(pathname)) {
|
|
489
|
+
url.pathname = route === 'compact'
|
|
490
|
+
? pathname.replace(/\/responses(?:\/compact)?$/i, '/responses/compact')
|
|
491
|
+
: pathname.replace(/\/responses(?:\/compact)?$/i, '/responses');
|
|
492
|
+
} else {
|
|
493
|
+
pathname = !pathname || pathname === '/' ? '/v1' : pathname;
|
|
494
|
+
url.pathname = `${pathname}/responses${route === 'compact' ? '/compact' : ''}`;
|
|
495
|
+
}
|
|
496
|
+
if (!launch.fullEndpoint) {
|
|
497
|
+
url.search = '';
|
|
498
|
+
url.hash = '';
|
|
499
|
+
}
|
|
500
|
+
for (const [key, value] of Object.entries(launch.queryParams || {})) {
|
|
501
|
+
url.searchParams.set(key, value);
|
|
502
|
+
}
|
|
503
|
+
return url.toString();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function buildUpstreamHeaders(launch = {}) {
|
|
507
|
+
const headers = {
|
|
508
|
+
accept: 'application/json, text/event-stream',
|
|
509
|
+
'content-type': 'application/json',
|
|
510
|
+
'user-agent': 'Codex-Overleaf-Link/provider-bridge',
|
|
511
|
+
...(launch.customHeaders || {})
|
|
512
|
+
};
|
|
513
|
+
if (!launch.apiKey || launch.authMode === 'none') return headers;
|
|
514
|
+
if (launch.authMode === 'x-api-key') headers['x-api-key'] = launch.apiKey;
|
|
515
|
+
else if (launch.authMode === 'api-key') headers['api-key'] = launch.apiKey;
|
|
516
|
+
else if (launch.authMode === 'custom' && launch.apiKeyHeaderName) {
|
|
517
|
+
headers[launch.apiKeyHeaderName] = launch.apiKey;
|
|
518
|
+
} else {
|
|
519
|
+
headers.authorization = `Bearer ${launch.apiKey}`;
|
|
520
|
+
}
|
|
521
|
+
return headers;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async function forwardUpstreamError(upstream, res, apiKey) {
|
|
525
|
+
const text = (await upstream.text()).slice(0, MAX_ERROR_BYTES);
|
|
526
|
+
let message = `Provider returned HTTP ${upstream.status}.`;
|
|
527
|
+
try {
|
|
528
|
+
const parsed = JSON.parse(text);
|
|
529
|
+
message = parsed?.error?.message || parsed?.message || message;
|
|
530
|
+
} catch (_error) {}
|
|
531
|
+
sendJsonError(res, upstream.status, 'provider_upstream_error', sanitizeProviderMessage(message, [apiKey]));
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function pruneHistory(history) {
|
|
535
|
+
while (history.size > MAX_HISTORY_ENTRIES || historyBytes(history) > MAX_HISTORY_BYTES) {
|
|
536
|
+
history.delete(history.keys().next().value);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function historyBytes(history) {
|
|
541
|
+
let total = 0;
|
|
542
|
+
for (const entry of history.values()) total += Number(entry.serializedBytes) || 0;
|
|
543
|
+
return total;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function serializedSize(value) {
|
|
547
|
+
try {
|
|
548
|
+
return Buffer.byteLength(JSON.stringify(value));
|
|
549
|
+
} catch (_error) {
|
|
550
|
+
return MAX_HISTORY_BYTES + 1;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function normalizeBridgeErrorCode(value) {
|
|
555
|
+
return [
|
|
556
|
+
'provider_connection_timeout',
|
|
557
|
+
'provider_response_invalid',
|
|
558
|
+
'provider_protocol_incompatible',
|
|
559
|
+
'provider_request_invalid',
|
|
560
|
+
'provider_request_too_large',
|
|
561
|
+
'provider_responses_continuation_repeated',
|
|
562
|
+
'provider_responses_continuation_unrecoverable',
|
|
563
|
+
'provider_continuation_history_limit'
|
|
564
|
+
].includes(value) ? value : 'provider_bridge_failed';
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function bridgeErrorMessage(code) {
|
|
568
|
+
return code === 'provider_connection_timeout'
|
|
569
|
+
? 'The provider request was cancelled or timed out.'
|
|
570
|
+
: 'The local Responses bridge failed.';
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function isAuthorized(req, token) {
|
|
574
|
+
const value = String(req.headers.authorization || '');
|
|
575
|
+
const supplied = value.startsWith('Bearer ') ? value.slice(7) : '';
|
|
576
|
+
const expectedBuffer = Buffer.from(token);
|
|
577
|
+
const suppliedBuffer = Buffer.from(supplied);
|
|
578
|
+
return expectedBuffer.length === suppliedBuffer.length
|
|
579
|
+
&& crypto.timingSafeEqual(expectedBuffer, suppliedBuffer);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function readJsonBody(req) {
|
|
583
|
+
return new Promise((resolve, reject) => {
|
|
584
|
+
const chunks = [];
|
|
585
|
+
let bytes = 0;
|
|
586
|
+
req.on('data', chunk => {
|
|
587
|
+
bytes += chunk.length;
|
|
588
|
+
if (bytes > MAX_REQUEST_BYTES) {
|
|
589
|
+
reject(providerError('provider_request_too_large', 'Provider request exceeded the local bridge limit.'));
|
|
590
|
+
req.destroy();
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
chunks.push(chunk);
|
|
594
|
+
});
|
|
595
|
+
req.on('end', () => {
|
|
596
|
+
try {
|
|
597
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}'));
|
|
598
|
+
} catch (_error) {
|
|
599
|
+
reject(providerError('provider_request_invalid', 'Codex sent an invalid Responses request.'));
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
req.on('error', reject);
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function sendJson(res, status, value) {
|
|
607
|
+
if (res.writableEnded) return;
|
|
608
|
+
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
|
|
609
|
+
res.end(JSON.stringify(value));
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function sendJsonError(res, status, code, message) {
|
|
613
|
+
sendJson(res, status, { error: { type: 'provider_error', code, message } });
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function listen(server) {
|
|
617
|
+
return new Promise((resolve, reject) => {
|
|
618
|
+
server.once('error', reject);
|
|
619
|
+
server.listen(0, '127.0.0.1', () => {
|
|
620
|
+
server.removeListener('error', reject);
|
|
621
|
+
resolve();
|
|
622
|
+
});
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function closeServer(server) {
|
|
627
|
+
return new Promise(resolve => {
|
|
628
|
+
server.close(() => resolve());
|
|
629
|
+
server.closeAllConnections?.();
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
module.exports = {
|
|
634
|
+
buildResponsesUrl,
|
|
635
|
+
startResponsesPassthroughBridge
|
|
636
|
+
};
|