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,281 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const http = require('node:http');
|
|
5
|
+
const {
|
|
6
|
+
buildAnthropicRequest,
|
|
7
|
+
rectifyAnthropicRequestForRetry
|
|
8
|
+
} = require('./anthropicBridgeRequest');
|
|
9
|
+
const {
|
|
10
|
+
convertAnthropicResponse,
|
|
11
|
+
streamAnthropicResponse
|
|
12
|
+
} = require('./anthropicBridgeResponse');
|
|
13
|
+
const { writeConvertedResponseAsSse } = require('./chatBridgeResponse');
|
|
14
|
+
const { classifyResponsesRoute } = require('./providerBridgeRoutes');
|
|
15
|
+
const { providerError } = require('./providerProfile');
|
|
16
|
+
const { sanitizeProviderMessage } = require('./providerRedaction');
|
|
17
|
+
|
|
18
|
+
const MAX_REQUEST_BYTES = 16 * 1024 * 1024;
|
|
19
|
+
const MAX_ERROR_BYTES = 64 * 1024;
|
|
20
|
+
const MAX_HISTORY_ENTRIES = 64;
|
|
21
|
+
|
|
22
|
+
async function startAnthropicMessagesBridge({ launch, signal } = {}) {
|
|
23
|
+
if (!launch?.baseUrl) throw providerError('provider_base_url_invalid', 'Anthropic routing requires a provider Base URL.');
|
|
24
|
+
const clientToken = crypto.randomBytes(32).toString('base64url');
|
|
25
|
+
const activeRequests = new Set();
|
|
26
|
+
const history = new Map();
|
|
27
|
+
const server = http.createServer((req, res) => {
|
|
28
|
+
handleRequest({ req, res, launch, clientToken, activeRequests, history }).catch(error => {
|
|
29
|
+
if (res.writableEnded) return;
|
|
30
|
+
const code = normalizeBridgeErrorCode(error?.code);
|
|
31
|
+
const message = sanitizeProviderMessage(error?.message, [launch.apiKey]) || 'The Anthropic provider bridge failed.';
|
|
32
|
+
if (res.headersSent) {
|
|
33
|
+
res.end(`event: response.failed\ndata: ${JSON.stringify({
|
|
34
|
+
type: 'response.failed',
|
|
35
|
+
sequence_number: 999999,
|
|
36
|
+
response: { status: 'failed', error: { code, message } }
|
|
37
|
+
})}\n\n`);
|
|
38
|
+
} else {
|
|
39
|
+
sendJsonError(res, code === 'provider_connection_timeout' ? 504 : 502, code, message);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
const timeoutMs = resolveTimeout(launch);
|
|
44
|
+
server.keepAliveTimeout = 5000;
|
|
45
|
+
server.requestTimeout = timeoutMs + 5000;
|
|
46
|
+
await listen(server);
|
|
47
|
+
const address = server.address();
|
|
48
|
+
let closed = false;
|
|
49
|
+
const close = async () => {
|
|
50
|
+
if (closed) return;
|
|
51
|
+
closed = true;
|
|
52
|
+
signal?.removeEventListener('abort', close);
|
|
53
|
+
for (const controller of activeRequests) controller.abort();
|
|
54
|
+
activeRequests.clear();
|
|
55
|
+
history.clear();
|
|
56
|
+
await closeServer(server);
|
|
57
|
+
};
|
|
58
|
+
signal?.addEventListener('abort', close, { once: true });
|
|
59
|
+
if (signal?.aborted) await close();
|
|
60
|
+
return { baseUrl: `http://127.0.0.1:${address.port}/v1`, clientToken, close };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function handleRequest({ req, res, launch, clientToken, activeRequests, history }) {
|
|
64
|
+
if (!isAuthorized(req, clientToken)) {
|
|
65
|
+
sendJsonError(res, 401, 'unauthorized', 'Local bridge authorization failed.');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const url = new URL(req.url || '/', 'http://127.0.0.1');
|
|
69
|
+
if (req.method === 'GET' && /\/models\/?$/.test(url.pathname)) {
|
|
70
|
+
sendJson(res, 200, { object: 'list', data: [{ id: launch.modelId, object: 'model', created: 0, owned_by: launch.providerName || 'anthropic' }] });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const responsesRoute = classifyResponsesRoute(req.method, url.pathname);
|
|
74
|
+
if (!responsesRoute) {
|
|
75
|
+
sendJsonError(res, 404, 'not_found', 'The local Anthropic bridge only exposes Responses, Responses Compact, and Models endpoints.');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const requestBody = await readJsonBody(req);
|
|
79
|
+
const previous = history.get(String(requestBody.previous_response_id || ''));
|
|
80
|
+
const translated = buildAnthropicRequest({
|
|
81
|
+
requestBody,
|
|
82
|
+
launch,
|
|
83
|
+
historyMessages: previous?.messages || []
|
|
84
|
+
});
|
|
85
|
+
const controller = new AbortController();
|
|
86
|
+
activeRequests.add(controller);
|
|
87
|
+
const onClientClose = () => {
|
|
88
|
+
if (!res.writableEnded) controller.abort();
|
|
89
|
+
};
|
|
90
|
+
req.on('aborted', onClientClose);
|
|
91
|
+
res.on('close', onClientClose);
|
|
92
|
+
const timeout = setTimeout(() => controller.abort(), resolveTimeout(launch));
|
|
93
|
+
try {
|
|
94
|
+
const upstream = await fetchWithRectification(launch, translated.body, controller.signal);
|
|
95
|
+
if (!upstream.response.ok) {
|
|
96
|
+
forwardUpstreamError(upstream.response, upstream.errorText, res, launch.apiKey);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const context = {
|
|
100
|
+
launch,
|
|
101
|
+
requestBody,
|
|
102
|
+
model: translated.body.model,
|
|
103
|
+
toolContext: translated.toolContext
|
|
104
|
+
};
|
|
105
|
+
const remember = converted => rememberHistory(history, converted.response.id, [
|
|
106
|
+
...(upstream.requestBody.messages || translated.messages),
|
|
107
|
+
{ role: 'assistant', content: converted.assistantBlocks }
|
|
108
|
+
]);
|
|
109
|
+
const contentType = String(upstream.response.headers.get('content-type') || '').toLowerCase();
|
|
110
|
+
if (requestBody.stream !== false) {
|
|
111
|
+
if (contentType.includes('text/event-stream')) {
|
|
112
|
+
await streamAnthropicResponse({ upstream: upstream.response, res, context, onComplete: remember });
|
|
113
|
+
} else {
|
|
114
|
+
const converted = convertAnthropicResponse(await upstream.response.json(), context);
|
|
115
|
+
writeConvertedResponseAsSse(res, converted, context, remember);
|
|
116
|
+
}
|
|
117
|
+
} else if (contentType.includes('text/event-stream')) {
|
|
118
|
+
let converted;
|
|
119
|
+
const sink = { writeHead() {}, write() {}, end() {} };
|
|
120
|
+
await streamAnthropicResponse({ upstream: upstream.response, res: sink, context, onComplete: value => { converted = value; } });
|
|
121
|
+
remember(converted);
|
|
122
|
+
sendJson(res, 200, converted.response);
|
|
123
|
+
} else {
|
|
124
|
+
const converted = convertAnthropicResponse(await upstream.response.json(), context);
|
|
125
|
+
remember(converted);
|
|
126
|
+
sendJson(res, 200, converted.response);
|
|
127
|
+
}
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if (controller.signal.aborted) throw providerError('provider_connection_timeout', 'The provider request was cancelled or timed out.');
|
|
130
|
+
throw error;
|
|
131
|
+
} finally {
|
|
132
|
+
clearTimeout(timeout);
|
|
133
|
+
activeRequests.delete(controller);
|
|
134
|
+
req.removeListener('aborted', onClientClose);
|
|
135
|
+
res.removeListener('close', onClientClose);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function fetchWithRectification(launch, body, signal) {
|
|
140
|
+
let requestBody = body;
|
|
141
|
+
let response = await fetch(buildAnthropicMessagesUrl(launch.baseUrl, launch), {
|
|
142
|
+
method: 'POST',
|
|
143
|
+
headers: buildAnthropicHeaders(launch),
|
|
144
|
+
body: JSON.stringify(body),
|
|
145
|
+
signal
|
|
146
|
+
});
|
|
147
|
+
if (response.ok) return { response, errorText: '', requestBody };
|
|
148
|
+
let errorText = (await response.text()).slice(0, MAX_ERROR_BYTES);
|
|
149
|
+
if (response.status === 400) {
|
|
150
|
+
const retryBody = JSON.parse(JSON.stringify(body));
|
|
151
|
+
if (rectifyAnthropicRequestForRetry(retryBody, errorText)) {
|
|
152
|
+
requestBody = retryBody;
|
|
153
|
+
response = await fetch(buildAnthropicMessagesUrl(launch.baseUrl, launch), {
|
|
154
|
+
method: 'POST',
|
|
155
|
+
headers: buildAnthropicHeaders(launch),
|
|
156
|
+
body: JSON.stringify(retryBody),
|
|
157
|
+
signal
|
|
158
|
+
});
|
|
159
|
+
errorText = response.ok ? '' : (await response.text()).slice(0, MAX_ERROR_BYTES);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return { response, errorText, requestBody };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function buildAnthropicMessagesUrl(baseUrl, launch = {}) {
|
|
166
|
+
const url = new URL(baseUrl);
|
|
167
|
+
if (!launch.fullEndpoint && !/\/v1\/messages\/?$/i.test(url.pathname.replace(/\/+$/, ''))) {
|
|
168
|
+
const path = url.pathname.replace(/\/+$/, '');
|
|
169
|
+
url.pathname = /\/v1$/i.test(path)
|
|
170
|
+
? `${path}/messages`
|
|
171
|
+
: `${!path || path === '/' ? '' : path}/v1/messages`;
|
|
172
|
+
}
|
|
173
|
+
url.hash = '';
|
|
174
|
+
for (const [key, value] of Object.entries(launch.queryParams || {})) url.searchParams.set(key, value);
|
|
175
|
+
return url.toString();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function buildAnthropicHeaders(launch = {}) {
|
|
179
|
+
const headers = {
|
|
180
|
+
accept: 'application/json, text/event-stream',
|
|
181
|
+
'content-type': 'application/json',
|
|
182
|
+
'user-agent': launch.impersonateClaudeCode ? 'claude-cli/2.1.0 (external, cli)' : 'Codex-Overleaf-Link/provider-bridge',
|
|
183
|
+
'anthropic-version': launch.anthropicVersion || '2023-06-01',
|
|
184
|
+
...(launch.customHeaders || {})
|
|
185
|
+
};
|
|
186
|
+
if (launch.anthropicBeta) headers['anthropic-beta'] = launch.anthropicBeta;
|
|
187
|
+
if (launch.impersonateClaudeCode) headers['x-app'] = 'cli';
|
|
188
|
+
if (!launch.apiKey || launch.authMode === 'none') return headers;
|
|
189
|
+
if (launch.authMode === 'bearer') headers.authorization = `Bearer ${launch.apiKey}`;
|
|
190
|
+
else if (launch.authMode === 'api-key') headers['api-key'] = launch.apiKey;
|
|
191
|
+
else if (launch.authMode === 'custom' && launch.apiKeyHeaderName) headers[launch.apiKeyHeaderName] = launch.apiKey;
|
|
192
|
+
else headers['x-api-key'] = launch.apiKey;
|
|
193
|
+
return headers;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function forwardUpstreamError(response, errorText, res, apiKey) {
|
|
197
|
+
let message = `Provider returned HTTP ${response.status}.`;
|
|
198
|
+
try {
|
|
199
|
+
const parsed = JSON.parse(errorText || '{}');
|
|
200
|
+
message = parsed?.error?.message || parsed?.message || message;
|
|
201
|
+
} catch (_error) {}
|
|
202
|
+
sendJsonError(res, response.status, 'provider_upstream_error', sanitizeProviderMessage(message, [apiKey]));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function rememberHistory(history, responseId, messages) {
|
|
206
|
+
history.delete(responseId);
|
|
207
|
+
history.set(responseId, { messages });
|
|
208
|
+
while (history.size > MAX_HISTORY_ENTRIES) history.delete(history.keys().next().value);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function resolveTimeout(launch = {}) {
|
|
212
|
+
const configured = Math.min(300000, Math.max(5000, Number(launch.requestTimeoutMs) || 30000));
|
|
213
|
+
const thinking = !['', 'none', 'off', 'disabled'].includes(String(launch.reasoningEffort || '').toLowerCase());
|
|
214
|
+
return thinking ? Math.max(120000, configured) : configured;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function normalizeBridgeErrorCode(value) {
|
|
218
|
+
return ['provider_connection_timeout', 'provider_response_invalid', 'provider_request_invalid', 'provider_upstream_error'].includes(value)
|
|
219
|
+
? value
|
|
220
|
+
: 'provider_bridge_failed';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function isAuthorized(req, token) {
|
|
224
|
+
const value = String(req.headers.authorization || '');
|
|
225
|
+
const supplied = value.startsWith('Bearer ') ? value.slice(7) : '';
|
|
226
|
+
const expectedBuffer = Buffer.from(token);
|
|
227
|
+
const suppliedBuffer = Buffer.from(supplied);
|
|
228
|
+
return expectedBuffer.length === suppliedBuffer.length && crypto.timingSafeEqual(expectedBuffer, suppliedBuffer);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function readJsonBody(req) {
|
|
232
|
+
return new Promise((resolve, reject) => {
|
|
233
|
+
const chunks = [];
|
|
234
|
+
let bytes = 0;
|
|
235
|
+
req.on('data', chunk => {
|
|
236
|
+
bytes += chunk.length;
|
|
237
|
+
if (bytes > MAX_REQUEST_BYTES) {
|
|
238
|
+
reject(providerError('provider_request_too_large', 'Provider request exceeded the local bridge limit.'));
|
|
239
|
+
req.destroy();
|
|
240
|
+
} else chunks.push(chunk);
|
|
241
|
+
});
|
|
242
|
+
req.on('end', () => {
|
|
243
|
+
try { resolve(JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}')); }
|
|
244
|
+
catch (_error) { reject(providerError('provider_request_invalid', 'Codex sent an invalid Responses request.')); }
|
|
245
|
+
});
|
|
246
|
+
req.on('error', reject);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function sendJson(res, status, value) {
|
|
251
|
+
if (res.writableEnded) return;
|
|
252
|
+
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
|
|
253
|
+
res.end(JSON.stringify(value));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function sendJsonError(res, status, code, message) {
|
|
257
|
+
sendJson(res, status, { error: { type: 'provider_error', code, message } });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function listen(server) {
|
|
261
|
+
return new Promise((resolve, reject) => {
|
|
262
|
+
server.once('error', reject);
|
|
263
|
+
server.listen(0, '127.0.0.1', () => {
|
|
264
|
+
server.removeListener('error', reject);
|
|
265
|
+
resolve();
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function closeServer(server) {
|
|
271
|
+
return new Promise(resolve => {
|
|
272
|
+
server.close(() => resolve());
|
|
273
|
+
server.closeAllConnections?.();
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
module.exports = {
|
|
278
|
+
buildAnthropicHeaders,
|
|
279
|
+
buildAnthropicMessagesUrl,
|
|
280
|
+
startAnthropicMessagesBridge
|
|
281
|
+
};
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
applyReasoningControl,
|
|
7
|
+
requiresReasoningContentReplay,
|
|
8
|
+
resolveReasoningMaxOutputTokens,
|
|
9
|
+
supportsToolChoice
|
|
10
|
+
} = require('./providerReasoning');
|
|
11
|
+
|
|
12
|
+
function buildChatRequest({ requestBody = {}, launch = {}, historyMessages = [] } = {}) {
|
|
13
|
+
const messages = cloneMessages(historyMessages);
|
|
14
|
+
const instructions = normalizeText(requestBody.instructions);
|
|
15
|
+
if (instructions && !messages.some(message => message.role === 'system')) {
|
|
16
|
+
messages.unshift({ role: 'system', content: instructions });
|
|
17
|
+
}
|
|
18
|
+
const model = normalizeText(requestBody.model) || launch.modelId;
|
|
19
|
+
const replayReasoning = requiresReasoningContentReplay(launch, model);
|
|
20
|
+
const { tools, toolKinds } = translateTools(requestBody.tools);
|
|
21
|
+
appendResponsesInput(messages, requestBody.input, { replayReasoning, toolKinds });
|
|
22
|
+
const body = {
|
|
23
|
+
...(launch.bodyOverrides || {}),
|
|
24
|
+
model,
|
|
25
|
+
messages,
|
|
26
|
+
stream: requestBody.stream !== false && launch.upstreamResponseMode !== 'buffered'
|
|
27
|
+
};
|
|
28
|
+
if (body.stream && launch.supportsStreamOptions) {
|
|
29
|
+
body.stream_options = { include_usage: true };
|
|
30
|
+
}
|
|
31
|
+
if (tools.length) {
|
|
32
|
+
body.tools = tools;
|
|
33
|
+
}
|
|
34
|
+
const toolChoice = translateToolChoice(requestBody.tool_choice, toolKinds);
|
|
35
|
+
if (toolChoice && supportsToolChoice(launch, model)) body.tool_choice = toolChoice;
|
|
36
|
+
copyNumber(requestBody, body, 'temperature');
|
|
37
|
+
copyNumber(requestBody, body, 'top_p');
|
|
38
|
+
const maxOutputTokens = resolveReasoningMaxOutputTokens(
|
|
39
|
+
requestBody.max_output_tokens ?? launch.maxOutputTokens,
|
|
40
|
+
requestBody,
|
|
41
|
+
launch,
|
|
42
|
+
model
|
|
43
|
+
);
|
|
44
|
+
if (Number.isFinite(Number(maxOutputTokens))) {
|
|
45
|
+
body.max_tokens = Math.max(1, Math.floor(Number(maxOutputTokens)));
|
|
46
|
+
}
|
|
47
|
+
if (launch.supportsParallelToolCalls && typeof requestBody.parallel_tool_calls === 'boolean') {
|
|
48
|
+
body.parallel_tool_calls = requestBody.parallel_tool_calls;
|
|
49
|
+
}
|
|
50
|
+
applyReasoningControl(body, requestBody, launch);
|
|
51
|
+
return { body, messages, toolKinds };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function appendResponsesInput(messages, input, { replayReasoning = false, toolKinds = new Map() } = {}) {
|
|
55
|
+
if (typeof input === 'string') {
|
|
56
|
+
if (input) messages.push({ role: 'user', content: input });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (!Array.isArray(input)) return;
|
|
60
|
+
let pendingToolCalls = [];
|
|
61
|
+
let pendingReasoning = '';
|
|
62
|
+
const flushToolCalls = () => {
|
|
63
|
+
if (!pendingToolCalls.length) return;
|
|
64
|
+
const assistant = {
|
|
65
|
+
role: 'assistant',
|
|
66
|
+
content: replayReasoning ? '' : null,
|
|
67
|
+
tool_calls: pendingToolCalls
|
|
68
|
+
};
|
|
69
|
+
if (replayReasoning) assistant.reasoning_content = pendingReasoning || ' ';
|
|
70
|
+
messages.push(assistant);
|
|
71
|
+
pendingToolCalls = [];
|
|
72
|
+
pendingReasoning = '';
|
|
73
|
+
};
|
|
74
|
+
for (const item of input) {
|
|
75
|
+
if (!item || typeof item !== 'object') continue;
|
|
76
|
+
if (item.type === 'reasoning') {
|
|
77
|
+
flushToolCalls();
|
|
78
|
+
pendingReasoning = extractReasoningText(item);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (item.type === 'function_call' || item.type === 'custom_tool_call' || item.type === 'tool_search_call') {
|
|
82
|
+
pendingToolCalls.push(toChatToolCall(item, toolKinds));
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
flushToolCalls();
|
|
86
|
+
if (item.type === 'function_call_output' || item.type === 'custom_tool_call_output' || item.type === 'computer_call_output' || item.type === 'tool_search_output') {
|
|
87
|
+
messages.push({
|
|
88
|
+
role: 'tool',
|
|
89
|
+
tool_call_id: normalizeText(item.call_id) || normalizeText(item.id),
|
|
90
|
+
content: normalizeToolOutput(item.output)
|
|
91
|
+
});
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (item.type === 'message' || item.role) {
|
|
95
|
+
const role = normalizeRole(item.role);
|
|
96
|
+
const content = translateMessageContent(item.content);
|
|
97
|
+
if (content !== '' && content !== null) {
|
|
98
|
+
const message = { role, content };
|
|
99
|
+
if (role === 'assistant' && replayReasoning && pendingReasoning) {
|
|
100
|
+
message.reasoning_content = pendingReasoning;
|
|
101
|
+
}
|
|
102
|
+
messages.push(message);
|
|
103
|
+
}
|
|
104
|
+
pendingReasoning = '';
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
flushToolCalls();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function extractReasoningText(item) {
|
|
111
|
+
const direct = normalizeText(item.reasoning_content || item.reasoning || item.content);
|
|
112
|
+
if (direct) return direct;
|
|
113
|
+
return (Array.isArray(item.summary) ? item.summary : [])
|
|
114
|
+
.map(part => normalizeText(part?.text || part?.content))
|
|
115
|
+
.filter(Boolean)
|
|
116
|
+
.join('\n');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function toChatToolCall(item, toolKinds) {
|
|
120
|
+
const spec = findToolSpec(toolKinds, item);
|
|
121
|
+
const isCustom = item.type === 'custom_tool_call' || spec?.kind === 'custom';
|
|
122
|
+
const rawArguments = isCustom
|
|
123
|
+
? JSON.stringify({ input: normalizeText(item.input) })
|
|
124
|
+
: normalizeArguments(item.arguments);
|
|
125
|
+
return {
|
|
126
|
+
id: normalizeText(item.call_id) || normalizeText(item.id),
|
|
127
|
+
type: 'function',
|
|
128
|
+
function: {
|
|
129
|
+
name: spec?.chatName || normalizeText(item.name),
|
|
130
|
+
arguments: rawArguments
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function translateMessageContent(content) {
|
|
136
|
+
if (typeof content === 'string') return content;
|
|
137
|
+
if (!Array.isArray(content)) return normalizeToolOutput(content);
|
|
138
|
+
const parts = [];
|
|
139
|
+
let hasStructuredPart = false;
|
|
140
|
+
for (const part of content) {
|
|
141
|
+
if (!part || typeof part !== 'object') continue;
|
|
142
|
+
if (part.type === 'input_text' || part.type === 'output_text' || part.type === 'text') {
|
|
143
|
+
parts.push({ type: 'text', text: normalizeText(part.text) });
|
|
144
|
+
} else if (part.type === 'input_image' || part.type === 'image_url') {
|
|
145
|
+
const url = normalizeText(part.image_url?.url || part.image_url || part.url);
|
|
146
|
+
if (url) {
|
|
147
|
+
hasStructuredPart = true;
|
|
148
|
+
parts.push({ type: 'image_url', image_url: { url } });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (!parts.length) return '';
|
|
153
|
+
if (!hasStructuredPart) return parts.map(part => part.text).join('');
|
|
154
|
+
return parts;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function translateTools(values) {
|
|
158
|
+
const tools = [];
|
|
159
|
+
const toolKinds = new Map();
|
|
160
|
+
for (const value of Array.isArray(values) ? values : []) {
|
|
161
|
+
if (value?.type === 'namespace' && Array.isArray(value.tools)) {
|
|
162
|
+
for (const child of value.tools) {
|
|
163
|
+
addTool(tools, toolKinds, child, normalizeText(value.name));
|
|
164
|
+
}
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
addTool(tools, toolKinds, value, '');
|
|
168
|
+
}
|
|
169
|
+
return { tools, toolKinds };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function addTool(tools, toolKinds, value, namespace) {
|
|
173
|
+
const originalName = normalizeText(value?.name || value?.function?.name)
|
|
174
|
+
|| (value?.type === 'tool_search' ? 'tool_search' : '');
|
|
175
|
+
if (!originalName) return;
|
|
176
|
+
const kind = value.type === 'custom'
|
|
177
|
+
? 'custom'
|
|
178
|
+
: value.type === 'tool_search'
|
|
179
|
+
? 'tool_search'
|
|
180
|
+
: 'function';
|
|
181
|
+
const chatName = uniqueChatToolName(namespace, originalName, toolKinds);
|
|
182
|
+
const spec = { kind, name: originalName, namespace, chatName };
|
|
183
|
+
toolKinds.set(chatName, spec);
|
|
184
|
+
tools.push({
|
|
185
|
+
type: 'function',
|
|
186
|
+
function: {
|
|
187
|
+
name: chatName,
|
|
188
|
+
description: normalizeText(value.description || value.function?.description),
|
|
189
|
+
parameters: kind === 'custom'
|
|
190
|
+
? {
|
|
191
|
+
type: 'object',
|
|
192
|
+
properties: { input: { type: 'string' } },
|
|
193
|
+
required: ['input'],
|
|
194
|
+
additionalProperties: false
|
|
195
|
+
}
|
|
196
|
+
: kind === 'tool_search'
|
|
197
|
+
? {
|
|
198
|
+
type: 'object',
|
|
199
|
+
properties: { query: { type: 'string' } },
|
|
200
|
+
required: ['query'],
|
|
201
|
+
additionalProperties: false
|
|
202
|
+
}
|
|
203
|
+
: normalizeParameters(value.parameters || value.function?.parameters)
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function translateToolChoice(value, toolKinds) {
|
|
209
|
+
if (value === 'auto' || value === 'required' || value === 'none') return value;
|
|
210
|
+
const name = normalizeText(value?.name || value?.function?.name);
|
|
211
|
+
const spec = name ? findToolSpec(toolKinds, { name, namespace: value?.namespace }) : null;
|
|
212
|
+
return name ? { type: 'function', function: { name: spec?.chatName || name } } : '';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function findToolSpec(toolKinds, item) {
|
|
216
|
+
const name = normalizeText(item?.name || item?.function?.name);
|
|
217
|
+
const namespace = normalizeText(item?.namespace);
|
|
218
|
+
for (const spec of toolKinds?.values?.() || []) {
|
|
219
|
+
if (typeof spec === 'object' && spec.name === name && (!namespace || spec.namespace === namespace)) {
|
|
220
|
+
return spec;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function uniqueChatToolName(namespace, name, toolKinds) {
|
|
227
|
+
const raw = [namespace, name].filter(Boolean).join('__').replace(/[^A-Za-z0-9_-]/g, '_');
|
|
228
|
+
const base = raw.length <= 64
|
|
229
|
+
? raw
|
|
230
|
+
: `${raw.slice(0, 51)}_${crypto.createHash('sha256').update(raw).digest('hex').slice(0, 12)}`;
|
|
231
|
+
if (!toolKinds.has(base)) return base;
|
|
232
|
+
const suffix = crypto.createHash('sha256').update(`${namespace}\0${name}`).digest('hex').slice(0, 8);
|
|
233
|
+
return `${base.slice(0, 55)}_${suffix}`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function normalizeParameters(value) {
|
|
237
|
+
return value && typeof value === 'object'
|
|
238
|
+
? value
|
|
239
|
+
: { type: 'object', properties: {}, additionalProperties: true };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function normalizeArguments(value) {
|
|
243
|
+
if (typeof value === 'string') return value;
|
|
244
|
+
if (value === undefined || value === null) return '{}';
|
|
245
|
+
try { return JSON.stringify(value); } catch (_error) { return '{}'; }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function normalizeToolOutput(value) {
|
|
249
|
+
if (typeof value === 'string') return value;
|
|
250
|
+
if (value === undefined || value === null) return '';
|
|
251
|
+
try { return JSON.stringify(value); } catch (_error) { return String(value); }
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function normalizeRole(value) {
|
|
255
|
+
const role = normalizeText(value).toLowerCase();
|
|
256
|
+
if (role === 'assistant' || role === 'tool' || role === 'system') return role;
|
|
257
|
+
if (role === 'developer') return 'system';
|
|
258
|
+
return 'user';
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function cloneMessages(messages) {
|
|
262
|
+
try { return JSON.parse(JSON.stringify(Array.isArray(messages) ? messages : [])); } catch (_error) { return []; }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function copyNumber(source, target, key) {
|
|
266
|
+
const value = Number(source?.[key]);
|
|
267
|
+
if (Number.isFinite(value)) target[key] = value;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function normalizeText(value) {
|
|
271
|
+
return typeof value === 'string' ? value : '';
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
module.exports = { buildChatRequest };
|