codex-overleaf-link 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +24 -23
  2. package/extension/bootstrap/manifest.template.json +2 -1
  3. package/extension/runtime-manifest.json +12 -6
  4. package/extension/src/background.js +7 -0
  5. package/extension/src/content/composerPanel.js +13 -3
  6. package/extension/src/content/contentRuntime.js +129 -64
  7. package/extension/src/content/contextTray.js +173 -9
  8. package/extension/src/content/markdownText.js +16 -1
  9. package/extension/src/content/mathText.js +233 -0
  10. package/extension/src/content/modelPicker.js +117 -106
  11. package/extension/src/content/modelPickerSupport.js +115 -0
  12. package/extension/src/content/nativeChannel.js +20 -1
  13. package/extension/src/content/panelMaintenance.js +2 -0
  14. package/extension/src/content/projectSettingsCoordinator.js +2 -0
  15. package/extension/src/content/providerSettingsCoordinator.js +413 -0
  16. package/extension/src/content/providerSettingsDialog.js +1006 -0
  17. package/extension/src/content/runTimelineView.js +24 -5
  18. package/extension/src/content/settingsPanel.js +33 -2
  19. package/extension/src/content/writebackOrchestrator.js +3 -1
  20. package/extension/src/page/overleafProjectSnapshot.js +29 -33
  21. package/extension/src/page/snapshotRouter.js +75 -8
  22. package/extension/src/page/writebackRouter.js +20 -33
  23. package/extension/src/pageBridge.js +1 -1
  24. package/extension/src/shared/agentTranscript.js +130 -0
  25. package/extension/src/shared/compatibility.js +3 -2
  26. package/extension/src/shared/failureReasons.js +75 -0
  27. package/extension/src/shared/i18n.js +22 -0
  28. package/extension/src/shared/models.js +1 -0
  29. package/extension/src/shared/providerProfiles.js +283 -0
  30. package/extension/src/shared/sessionState.js +12 -2
  31. package/extension/src/shared/storageDb.js +1 -1
  32. package/extension/styles/panel.css +689 -1
  33. package/extension/vendor/katex/LICENSE +21 -0
  34. package/extension/vendor/katex/VERSION +2 -0
  35. package/extension/vendor/katex/fonts/KaTeX_AMS-Regular.ttf +0 -0
  36. package/extension/vendor/katex/fonts/KaTeX_AMS-Regular.woff +0 -0
  37. package/extension/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  38. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  39. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  40. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  41. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  42. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  43. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  44. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  45. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  46. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  47. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  48. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  49. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  50. package/extension/vendor/katex/fonts/KaTeX_Main-Bold.ttf +0 -0
  51. package/extension/vendor/katex/fonts/KaTeX_Main-Bold.woff +0 -0
  52. package/extension/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  53. package/extension/vendor/katex/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  54. package/extension/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  55. package/extension/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  56. package/extension/vendor/katex/fonts/KaTeX_Main-Italic.ttf +0 -0
  57. package/extension/vendor/katex/fonts/KaTeX_Main-Italic.woff +0 -0
  58. package/extension/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  59. package/extension/vendor/katex/fonts/KaTeX_Main-Regular.ttf +0 -0
  60. package/extension/vendor/katex/fonts/KaTeX_Main-Regular.woff +0 -0
  61. package/extension/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  62. package/extension/vendor/katex/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  63. package/extension/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  64. package/extension/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  65. package/extension/vendor/katex/fonts/KaTeX_Math-Italic.ttf +0 -0
  66. package/extension/vendor/katex/fonts/KaTeX_Math-Italic.woff +0 -0
  67. package/extension/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  68. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  69. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  70. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  71. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  72. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  73. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  74. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  75. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  76. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  77. package/extension/vendor/katex/fonts/KaTeX_Script-Regular.ttf +0 -0
  78. package/extension/vendor/katex/fonts/KaTeX_Script-Regular.woff +0 -0
  79. package/extension/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  80. package/extension/vendor/katex/fonts/KaTeX_Size1-Regular.ttf +0 -0
  81. package/extension/vendor/katex/fonts/KaTeX_Size1-Regular.woff +0 -0
  82. package/extension/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  83. package/extension/vendor/katex/fonts/KaTeX_Size2-Regular.ttf +0 -0
  84. package/extension/vendor/katex/fonts/KaTeX_Size2-Regular.woff +0 -0
  85. package/extension/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  86. package/extension/vendor/katex/fonts/KaTeX_Size3-Regular.ttf +0 -0
  87. package/extension/vendor/katex/fonts/KaTeX_Size3-Regular.woff +0 -0
  88. package/extension/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  89. package/extension/vendor/katex/fonts/KaTeX_Size4-Regular.ttf +0 -0
  90. package/extension/vendor/katex/fonts/KaTeX_Size4-Regular.woff +0 -0
  91. package/extension/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  92. package/extension/vendor/katex/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  93. package/extension/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  94. package/extension/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  95. package/extension/vendor/katex/katex.min.css +1 -0
  96. package/extension/vendor/katex/katex.min.js +1 -0
  97. package/native-host/src/anthropicBridgeRequest.js +435 -0
  98. package/native-host/src/anthropicBridgeResponse.js +431 -0
  99. package/native-host/src/anthropicMessagesBridge.js +281 -0
  100. package/native-host/src/chatBridgeRequest.js +274 -0
  101. package/native-host/src/chatBridgeResponse.js +594 -0
  102. package/native-host/src/chatCompletionsBridge.js +485 -0
  103. package/native-host/src/codexCommand.js +25 -0
  104. package/native-host/src/codexPromptAssembly.js +9 -0
  105. package/native-host/src/codexProviderLaunch.js +213 -0
  106. package/native-host/src/codexProviderTest.js +196 -0
  107. package/native-host/src/codexSessionRunner.js +29 -25
  108. package/native-host/src/managedInstall.js +1 -0
  109. package/native-host/src/providerBridgeRoutes.js +11 -0
  110. package/native-host/src/providerModelCatalog.js +107 -0
  111. package/native-host/src/providerProfile.js +454 -0
  112. package/native-host/src/providerReasoning.js +235 -0
  113. package/native-host/src/providerRedaction.js +18 -0
  114. package/native-host/src/providerRuntime.js +365 -0
  115. package/native-host/src/providerStore.js +691 -0
  116. package/native-host/src/readProgressGuard.js +360 -0
  117. package/native-host/src/taskRunnerRuntime.js +39 -3
  118. package/native-host/src/updateArchive.js +1 -0
  119. package/package.json +2 -1
@@ -0,0 +1,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 };