codex-overleaf-link 1.9.13 → 2.1.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 +24 -23
- package/extension/bootstrap/manifest.template.json +2 -1
- package/extension/runtime-manifest.json +13 -6
- package/extension/src/background.js +7 -0
- package/extension/src/content/composerPanel.js +13 -3
- package/extension/src/content/contentRuntime.js +130 -65
- 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 +981 -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/trackedChangesLifecycle.js +154 -26
- package/extension/src/page/writebackRouter.js +31 -60
- package/extension/src/pageBridge.js +1 -1
- package/extension/src/shared/agentTranscript.js +130 -0
- package/extension/src/shared/compatibility.js +10 -7
- 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 +27 -19
- package/extension/src/shared/storageRunActions.js +99 -0
- package/extension/styles/panel.css +587 -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 +40 -4
- package/native-host/src/updateArchive.js +1 -0
- package/native-host/src/updateTrust.js +2 -1
- package/package.json +2 -1
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { startChatCompletionsBridge } = require('./chatCompletionsBridge');
|
|
4
|
+
const { startAnthropicMessagesBridge } = require('./anthropicMessagesBridge');
|
|
5
|
+
const { prepareProviderModelCatalog } = require('./providerModelCatalog');
|
|
6
|
+
|
|
7
|
+
const PROVIDER_CONFIG_ID = 'codex_overleaf_custom';
|
|
8
|
+
const PROVIDER_API_KEY_ENV = 'CODEX_OVERLEAF_PROVIDER_API_KEY';
|
|
9
|
+
const LOOPBACK_NO_PROXY_HOSTS = ['127.0.0.1', 'localhost', '::1'];
|
|
10
|
+
|
|
11
|
+
function createProviderLaunch({ profile, secret = '', modelId, wireApi, reasoningEffort = '', upstreamResponseMode = '' }) {
|
|
12
|
+
const model = profile.models.find(item => item.id === modelId) || {};
|
|
13
|
+
const requestedResponseMode = ['streaming', 'buffered'].includes(upstreamResponseMode)
|
|
14
|
+
? upstreamResponseMode
|
|
15
|
+
: model.upstreamResponseMode || 'auto';
|
|
16
|
+
const resolvedResponseMode = requestedResponseMode === 'auto'
|
|
17
|
+
? (model.resolvedUpstreamResponseMode || 'streaming')
|
|
18
|
+
: requestedResponseMode;
|
|
19
|
+
return Object.freeze({
|
|
20
|
+
providerId: profile.id,
|
|
21
|
+
providerRevision: profile.revision,
|
|
22
|
+
providerName: profile.name,
|
|
23
|
+
baseUrl: profile.baseUrl,
|
|
24
|
+
modelId,
|
|
25
|
+
wireApi,
|
|
26
|
+
upstreamResponseMode: resolvedResponseMode === 'buffered' ? 'buffered' : 'streaming',
|
|
27
|
+
requestTimeoutMs: profile.requestTimeoutMs,
|
|
28
|
+
reasoningAdapter: profile.reasoningAdapter || 'auto',
|
|
29
|
+
reasoningCapability: profile.reasoningCapability || 'auto',
|
|
30
|
+
reasoningEffort,
|
|
31
|
+
reasoningEfforts: model.reasoningEfforts?.length
|
|
32
|
+
? model.reasoningEfforts.slice()
|
|
33
|
+
: reasoningEffort
|
|
34
|
+
? [reasoningEffort]
|
|
35
|
+
: [],
|
|
36
|
+
models: profile.models.map(item => ({ ...item })),
|
|
37
|
+
contextWindow: model.contextWindow || profile.contextWindow,
|
|
38
|
+
supportsParallelToolCalls: model.supportsParallelToolCalls ?? profile.supportsParallelToolCalls,
|
|
39
|
+
supportsStreamOptions: Boolean(profile.supportsStreamOptions),
|
|
40
|
+
inputModalities: model.inputModalities || profile.inputModalities,
|
|
41
|
+
authMode: profile.authMode || 'bearer',
|
|
42
|
+
apiKeyHeaderName: profile.apiKeyHeaderName || '',
|
|
43
|
+
fullEndpoint: Boolean(profile.fullEndpoint),
|
|
44
|
+
customHeaders: { ...(profile.customHeaders || {}) },
|
|
45
|
+
queryParams: { ...(profile.queryParams || {}) },
|
|
46
|
+
bodyOverrides: { ...(profile.bodyOverrides || {}) },
|
|
47
|
+
anthropicVersion: profile.anthropicVersion || '2023-06-01',
|
|
48
|
+
anthropicBeta: profile.anthropicBeta || '',
|
|
49
|
+
anthropicThinkingMode: profile.anthropicThinkingMode || 'budget',
|
|
50
|
+
anthropicPromptCaching: profile.anthropicPromptCaching === true,
|
|
51
|
+
impersonateClaudeCode: profile.impersonateClaudeCode === true,
|
|
52
|
+
maxOutputTokens: profile.maxOutputTokens || 32768,
|
|
53
|
+
apiKey: secret
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function prepareProviderLaunch(launch, { signal } = {}) {
|
|
58
|
+
const catalog = prepareProviderModelCatalog(launch);
|
|
59
|
+
if (!catalog.launch || !['chat', 'anthropic'].includes(catalog.launch.wireApi)) {
|
|
60
|
+
return { launch: catalog.launch, close: async () => catalog.close() };
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const routedWireApi = catalog.launch.wireApi;
|
|
64
|
+
const bridge = routedWireApi === 'anthropic'
|
|
65
|
+
? await startAnthropicMessagesBridge({ launch: catalog.launch, signal })
|
|
66
|
+
: await startChatCompletionsBridge({ launch: catalog.launch, signal });
|
|
67
|
+
return {
|
|
68
|
+
launch: Object.freeze({
|
|
69
|
+
...catalog.launch,
|
|
70
|
+
upstreamBaseUrl: catalog.launch.baseUrl,
|
|
71
|
+
baseUrl: bridge.baseUrl,
|
|
72
|
+
wireApi: 'responses',
|
|
73
|
+
routedWireApi,
|
|
74
|
+
apiKey: bridge.clientToken,
|
|
75
|
+
authMode: 'bearer',
|
|
76
|
+
apiKeyHeaderName: '',
|
|
77
|
+
fullEndpoint: false,
|
|
78
|
+
customHeaders: {},
|
|
79
|
+
queryParams: {},
|
|
80
|
+
bodyOverrides: {}
|
|
81
|
+
}),
|
|
82
|
+
close: async () => {
|
|
83
|
+
await bridge.close();
|
|
84
|
+
catalog.close();
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
} catch (error) {
|
|
88
|
+
catalog.close();
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function buildProviderConfigArgs(launch) {
|
|
94
|
+
if (!launch) {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
const values = [
|
|
98
|
+
['model', launch.modelId],
|
|
99
|
+
['model_provider', PROVIDER_CONFIG_ID],
|
|
100
|
+
[`model_providers.${PROVIDER_CONFIG_ID}.name`, launch.providerName],
|
|
101
|
+
[`model_providers.${PROVIDER_CONFIG_ID}.base_url`, launch.baseUrl],
|
|
102
|
+
[`model_providers.${PROVIDER_CONFIG_ID}.wire_api`, launch.wireApi]
|
|
103
|
+
];
|
|
104
|
+
if (launch.apiKey && (launch.authMode || 'bearer') === 'bearer') {
|
|
105
|
+
values.push([`model_providers.${PROVIDER_CONFIG_ID}.env_key`, PROVIDER_API_KEY_ENV]);
|
|
106
|
+
}
|
|
107
|
+
const args = [];
|
|
108
|
+
for (const [key, value] of values) {
|
|
109
|
+
args.push('-c', `${key}=${encodeTomlString(value)}`);
|
|
110
|
+
}
|
|
111
|
+
args.push('-c', `model_providers.${PROVIDER_CONFIG_ID}.requires_openai_auth=false`);
|
|
112
|
+
if (launch.modelCatalogPath) {
|
|
113
|
+
args.push('-c', `model_catalog_json=${encodeTomlString(launch.modelCatalogPath)}`);
|
|
114
|
+
}
|
|
115
|
+
if (Object.keys(launch.customHeaders || {}).length) {
|
|
116
|
+
args.push('-c', `model_providers.${PROVIDER_CONFIG_ID}.http_headers=${encodeTomlTable(launch.customHeaders)}`);
|
|
117
|
+
}
|
|
118
|
+
if (Object.keys(launch.queryParams || {}).length) {
|
|
119
|
+
args.push('-c', `model_providers.${PROVIDER_CONFIG_ID}.query_params=${encodeTomlTable(launch.queryParams)}`);
|
|
120
|
+
}
|
|
121
|
+
const authHeader = resolveAuthHeader(launch);
|
|
122
|
+
if (launch.apiKey && authHeader) {
|
|
123
|
+
args.push('-c', `model_providers.${PROVIDER_CONFIG_ID}.env_http_headers=${encodeTomlTable({
|
|
124
|
+
[authHeader]: PROVIDER_API_KEY_ENV
|
|
125
|
+
})}`);
|
|
126
|
+
}
|
|
127
|
+
return args;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function applyProviderEnvironment(env, launch) {
|
|
131
|
+
const result = { ...(env || process.env) };
|
|
132
|
+
delete result[PROVIDER_API_KEY_ENV];
|
|
133
|
+
if (launch?.apiKey) {
|
|
134
|
+
result[PROVIDER_API_KEY_ENV] = launch.apiKey;
|
|
135
|
+
}
|
|
136
|
+
if (isLoopbackBaseUrl(launch?.baseUrl)) {
|
|
137
|
+
const noProxy = mergeNoProxyValues(result.NO_PROXY, result.no_proxy, LOOPBACK_NO_PROXY_HOSTS);
|
|
138
|
+
result.NO_PROXY = noProxy;
|
|
139
|
+
result.no_proxy = noProxy;
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isLoopbackBaseUrl(value) {
|
|
145
|
+
try {
|
|
146
|
+
const hostname = new URL(String(value || '')).hostname.toLowerCase().replace(/^\[|\]$/g, '');
|
|
147
|
+
return LOOPBACK_NO_PROXY_HOSTS.includes(hostname);
|
|
148
|
+
} catch (_error) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function mergeNoProxyValues(...values) {
|
|
154
|
+
const entries = [];
|
|
155
|
+
const seen = new Set();
|
|
156
|
+
for (const value of values.flat()) {
|
|
157
|
+
for (const entry of String(value || '').split(',')) {
|
|
158
|
+
const normalized = entry.trim();
|
|
159
|
+
const key = normalized.toLowerCase();
|
|
160
|
+
if (!normalized || seen.has(key)) continue;
|
|
161
|
+
seen.add(key);
|
|
162
|
+
entries.push(normalized);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return entries.includes('*') ? '*' : entries.join(',');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function buildProviderSnapshot(launch) {
|
|
169
|
+
if (!launch) {
|
|
170
|
+
return {
|
|
171
|
+
providerId: 'builtin',
|
|
172
|
+
providerRevision: 0,
|
|
173
|
+
providerName: 'Built-in Codex',
|
|
174
|
+
providerKind: 'builtin'
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
providerId: launch.providerId,
|
|
179
|
+
providerRevision: launch.providerRevision,
|
|
180
|
+
providerName: launch.providerName,
|
|
181
|
+
providerKind: 'custom',
|
|
182
|
+
modelId: launch.modelId,
|
|
183
|
+
wireApi: launch.routedWireApi || launch.wireApi,
|
|
184
|
+
upstreamResponseMode: launch.upstreamResponseMode || 'streaming',
|
|
185
|
+
requestTimeoutMs: launch.requestTimeoutMs
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function encodeTomlString(value) {
|
|
190
|
+
return JSON.stringify(String(value ?? ''));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function encodeTomlTable(value) {
|
|
194
|
+
return `{${Object.entries(value || {}).map(([key, item]) => (
|
|
195
|
+
`${encodeTomlString(key)}=${encodeTomlString(item)}`
|
|
196
|
+
)).join(',')}}`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function resolveAuthHeader(launch) {
|
|
200
|
+
if (!launch || launch.authMode === 'none' || (launch.authMode || 'bearer') === 'bearer') return '';
|
|
201
|
+
if (launch.authMode === 'x-api-key') return 'x-api-key';
|
|
202
|
+
if (launch.authMode === 'api-key') return 'api-key';
|
|
203
|
+
return launch.apiKeyHeaderName || '';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
module.exports = {
|
|
207
|
+
PROVIDER_API_KEY_ENV,
|
|
208
|
+
applyProviderEnvironment,
|
|
209
|
+
buildProviderConfigArgs,
|
|
210
|
+
buildProviderSnapshot,
|
|
211
|
+
createProviderLaunch,
|
|
212
|
+
prepareProviderLaunch
|
|
213
|
+
};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawn, spawnSync } = require('node:child_process');
|
|
4
|
+
const crypto = require('node:crypto');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
const { resolveCodexCommand, shouldUseShellForCommand } = require('./codexCommand');
|
|
9
|
+
const { applyProviderEnvironment, buildProviderConfigArgs, prepareProviderLaunch } = require('./codexProviderLaunch');
|
|
10
|
+
const { providerError } = require('./providerProfile');
|
|
11
|
+
|
|
12
|
+
const MAX_OUTPUT_BYTES = 256 * 1024;
|
|
13
|
+
|
|
14
|
+
async function runProviderConnectionTest({ launch, env = process.env, signal } = {}) {
|
|
15
|
+
const prepared = await prepareProviderLaunch(launch, { signal });
|
|
16
|
+
try {
|
|
17
|
+
return await runProviderConnectionTestProcess({ launch: prepared.launch, env, signal });
|
|
18
|
+
} finally {
|
|
19
|
+
await prepared.close();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function runProviderConnectionTestProcess({ launch, env = process.env, signal } = {}) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-overleaf-provider-test-'));
|
|
26
|
+
const codexHome = path.join(root, 'codex-home');
|
|
27
|
+
const workspace = path.join(root, 'workspace');
|
|
28
|
+
fs.mkdirSync(codexHome, { recursive: true, mode: 0o700 });
|
|
29
|
+
fs.mkdirSync(workspace, { recursive: true, mode: 0o700 });
|
|
30
|
+
const verificationToken = `provider-tool-check-${crypto.randomUUID()}`;
|
|
31
|
+
const secondFileName = `provider-check-${crypto.randomUUID()}.txt`;
|
|
32
|
+
fs.writeFileSync(path.join(workspace, 'provider-step-one.txt'), `${secondFileName}\n`, { mode: 0o600 });
|
|
33
|
+
fs.writeFileSync(path.join(workspace, secondFileName), `${verificationToken}\n`, { mode: 0o600 });
|
|
34
|
+
const childEnv = applyProviderEnvironment({
|
|
35
|
+
...env,
|
|
36
|
+
CODEX_HOME: codexHome
|
|
37
|
+
}, launch);
|
|
38
|
+
const command = resolveCodexCommand(childEnv);
|
|
39
|
+
if (!command) {
|
|
40
|
+
cleanup(root);
|
|
41
|
+
reject(providerError('codex_not_found', 'Codex CLI was not found locally.'));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const args = [
|
|
45
|
+
...buildProviderConfigArgs(launch),
|
|
46
|
+
'exec',
|
|
47
|
+
'--skip-git-repo-check',
|
|
48
|
+
'--sandbox',
|
|
49
|
+
'read-only',
|
|
50
|
+
'Use the shell tool to read provider-step-one.txt. It contains the name of a second file. Then use a separate shell tool call to read that second file. Reply only with the exact contents of the second file.'
|
|
51
|
+
];
|
|
52
|
+
const startedAt = Date.now();
|
|
53
|
+
let child;
|
|
54
|
+
try {
|
|
55
|
+
child = spawn(command, args, {
|
|
56
|
+
cwd: workspace,
|
|
57
|
+
env: childEnv,
|
|
58
|
+
shell: shouldUseShellForCommand(command, childEnv),
|
|
59
|
+
detached: process.platform !== 'win32',
|
|
60
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
61
|
+
});
|
|
62
|
+
} catch (error) {
|
|
63
|
+
cleanup(root);
|
|
64
|
+
reject(providerError('provider_test_spawn_failed', 'Codex could not start the provider connection test.', { cause: error }));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
let stdout = '';
|
|
68
|
+
let stderr = '';
|
|
69
|
+
let settled = false;
|
|
70
|
+
const timeoutMs = Math.min(120000, Math.max(5000, Number(launch.requestTimeoutMs) || 30000));
|
|
71
|
+
const timer = setTimeout(() => finish(providerError('provider_connection_timeout', 'The provider connection test timed out.')), timeoutMs);
|
|
72
|
+
const onAbort = () => finish(providerError('provider_test_cancelled', 'Provider connection test was cancelled.'));
|
|
73
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
74
|
+
|
|
75
|
+
child.stdout.setEncoding('utf8');
|
|
76
|
+
child.stderr.setEncoding('utf8');
|
|
77
|
+
child.stdout.on('data', chunk => appendOutput('stdout', chunk));
|
|
78
|
+
child.stderr.on('data', chunk => appendOutput('stderr', chunk));
|
|
79
|
+
child.on('error', error => finish(providerError('provider_test_spawn_failed', 'Codex could not start the provider connection test.', { cause: error })));
|
|
80
|
+
child.on('close', code => {
|
|
81
|
+
if (hasLeakedToolControlTokens(`${stdout}\n${stderr}`)) {
|
|
82
|
+
finish(providerError(
|
|
83
|
+
'provider_stream_tool_parse_failed',
|
|
84
|
+
'The provider leaked unparsed tool-call control tokens while streaming.'
|
|
85
|
+
));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (code === 0 && stdout.includes(verificationToken)) {
|
|
89
|
+
finish(null, {
|
|
90
|
+
durationMs: Date.now() - startedAt,
|
|
91
|
+
capabilities: {
|
|
92
|
+
text: true,
|
|
93
|
+
streaming: true,
|
|
94
|
+
agentTools: true,
|
|
95
|
+
toolRoundTrip: true
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (code === 0) {
|
|
101
|
+
finish(providerError(
|
|
102
|
+
'provider_agent_tools_incompatible',
|
|
103
|
+
'The provider returned text but did not complete the required Codex tool call.'
|
|
104
|
+
));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
finish(classifyProviderFailure(stderr || stdout, code));
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
function appendOutput(target, chunk) {
|
|
111
|
+
if (settled) return;
|
|
112
|
+
if (target === 'stdout') stdout += chunk;
|
|
113
|
+
else stderr += chunk;
|
|
114
|
+
if (Buffer.byteLength(stdout) + Buffer.byteLength(stderr) > MAX_OUTPUT_BYTES) {
|
|
115
|
+
finish(providerError('provider_response_invalid', 'Provider connection test exceeded the output limit.'));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function finish(error, result) {
|
|
120
|
+
if (settled) return;
|
|
121
|
+
settled = true;
|
|
122
|
+
clearTimeout(timer);
|
|
123
|
+
signal?.removeEventListener('abort', onAbort);
|
|
124
|
+
if (error) terminateProcessTree(child);
|
|
125
|
+
cleanup(root);
|
|
126
|
+
if (error) reject(error);
|
|
127
|
+
else resolve(result);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function classifyProviderFailure(text, exitCode) {
|
|
133
|
+
const message = String(text || '');
|
|
134
|
+
if (hasLeakedToolControlTokens(message) || /provider_stream_tool_parse_failed/i.test(message)) {
|
|
135
|
+
return providerError('provider_stream_tool_parse_failed', 'The provider failed to parse streaming tool calls.');
|
|
136
|
+
}
|
|
137
|
+
if (/\b401\b|unauthori[sz]ed|invalid api key|incorrect api key|authentication/i.test(message)) {
|
|
138
|
+
return providerError('provider_auth_rejected', 'The provider rejected the API key.');
|
|
139
|
+
}
|
|
140
|
+
if (/\b403\b|forbidden/i.test(message)) {
|
|
141
|
+
return providerError('provider_auth_rejected', 'The provider denied access for this credential.');
|
|
142
|
+
}
|
|
143
|
+
if (/\b429\b|rate.?limit|too many requests/i.test(message)) {
|
|
144
|
+
return providerError('provider_rate_limited', 'The provider rate limit was reached.');
|
|
145
|
+
}
|
|
146
|
+
if (/model.{0,40}(not found|does not exist|unknown)|\b404\b.{0,80}model/i.test(message)) {
|
|
147
|
+
return providerError('provider_model_not_found', 'The configured model was not found.');
|
|
148
|
+
}
|
|
149
|
+
if (/certificate|self.signed|tls|ssl/i.test(message)) {
|
|
150
|
+
return providerError('provider_tls_failed', 'TLS verification failed for the provider endpoint.');
|
|
151
|
+
}
|
|
152
|
+
if (/enotfound|eai_again|name or service not known|dns/i.test(message)) {
|
|
153
|
+
return providerError('provider_dns_failed', 'The provider hostname could not be resolved.');
|
|
154
|
+
}
|
|
155
|
+
if (/\b404\b|\b405\b|cannot\s+(?:post|get)|(?:route|endpoint).{0,40}(?:not found|unsupported)|unsupported.{0,30}(?:endpoint|api|protocol)|responses.{0,30}(?:unsupported|not found)|chat\/completions.{0,30}(?:unsupported|not found)|v1\/messages.{0,30}(?:unsupported|not found)/i.test(message)) {
|
|
156
|
+
return providerError('provider_protocol_incompatible', 'The endpoint is incompatible with this API protocol.');
|
|
157
|
+
}
|
|
158
|
+
if (/\b400\b|\b415\b|\b422\b|invalid.{0,30}(?:request|parameter|field)|unknown.{0,20}(?:field|parameter)|unsupported.{0,30}(?:field|parameter|tool|schema)|max[_ -]?tokens|context.{0,30}(?:length|window|limit)/i.test(message)) {
|
|
159
|
+
return providerError('provider_request_rejected', 'The provider rejected this protocol probe request. Review the model and compatibility settings.');
|
|
160
|
+
}
|
|
161
|
+
if (/timed?\s*out|timeout/i.test(message)) {
|
|
162
|
+
return providerError('provider_connection_timeout', 'The provider connection timed out.');
|
|
163
|
+
}
|
|
164
|
+
return providerError('provider_response_invalid', `Provider connection test failed with exit code ${exitCode}.`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function hasLeakedToolControlTokens(value) {
|
|
168
|
+
return /<\|tool_call_(?:begin|argument_begin|end)\|>/i.test(String(value || ''));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function terminateProcessTree(child) {
|
|
172
|
+
if (!child?.pid) return;
|
|
173
|
+
try {
|
|
174
|
+
if (process.platform === 'win32') {
|
|
175
|
+
spawnSync('taskkill', ['/pid', String(child.pid), '/t', '/f'], { stdio: 'ignore', windowsHide: true });
|
|
176
|
+
} else {
|
|
177
|
+
process.kill(-child.pid, 'SIGTERM');
|
|
178
|
+
setTimeout(() => {
|
|
179
|
+
try { process.kill(-child.pid, 'SIGKILL'); } catch (_error) {}
|
|
180
|
+
}, 500).unref?.();
|
|
181
|
+
}
|
|
182
|
+
} catch (_error) {
|
|
183
|
+
try { child.kill('SIGKILL'); } catch (_killError) {}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function cleanup(root) {
|
|
188
|
+
try {
|
|
189
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
190
|
+
} catch (_error) {}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = {
|
|
194
|
+
classifyProviderFailure,
|
|
195
|
+
runProviderConnectionTest
|
|
196
|
+
};
|
|
@@ -19,6 +19,9 @@ const {
|
|
|
19
19
|
loadSelectedProjectSkills
|
|
20
20
|
} = require('./localSkills');
|
|
21
21
|
const { createSubagentBroker } = require('./subagentBroker');
|
|
22
|
+
const { resolveCodexCommand, shouldUseShellForCommand } = require('./codexCommand');
|
|
23
|
+
const { applyProviderEnvironment, buildProviderConfigArgs, prepareProviderLaunch } = require('./codexProviderLaunch');
|
|
24
|
+
const { buildReadProgressRules, createReadProgressController } = require('./readProgressGuard');
|
|
22
25
|
|
|
23
26
|
const PARALLEL_SUBAGENTS_SKILL_ID = 'parallel-subagents';
|
|
24
27
|
|
|
@@ -27,7 +30,7 @@ const MAX_TURN_ATTACHMENT_BYTES = 12 * 1024 * 1024;
|
|
|
27
30
|
const MAX_TURN_ATTACHMENTS = 8;
|
|
28
31
|
const MAX_TURN_ATTACHMENT_TOTAL_BYTES = MAX_TURN_ATTACHMENT_BYTES * MAX_TURN_ATTACHMENTS;
|
|
29
32
|
|
|
30
|
-
async function runCodexSession({ params = {}, env = process.env, emit = () => {}, rootDir, executeCodex, signal } = {}) {
|
|
33
|
+
async function runCodexSession({ params = {}, env = process.env, emit = () => {}, rootDir, executeCodex, providerLaunch, signal } = {}) {
|
|
31
34
|
throwIfAborted(signal);
|
|
32
35
|
const projectId = params.projectId || params.project?.projectId || params.project?.id || params.project?.url || 'overleaf-project';
|
|
33
36
|
const skillInvocation = normalizeSkillInvocation(params.skillInvocation);
|
|
@@ -117,6 +120,7 @@ async function runCodexSession({ params = {}, env = process.env, emit = () => {}
|
|
|
117
120
|
disableCodexOverleafSkillIds: [PARALLEL_SUBAGENTS_SKILL_ID],
|
|
118
121
|
sandboxMode: settings.sandboxMode,
|
|
119
122
|
approvalPolicy: settings.approvalPolicy,
|
|
123
|
+
providerLaunch,
|
|
120
124
|
env,
|
|
121
125
|
// Worker raw events stay out of the parent timeline (spec §8);
|
|
122
126
|
// lifecycle events come from the broker, full text from result files.
|
|
@@ -152,6 +156,7 @@ async function runCodexSession({ params = {}, env = process.env, emit = () => {}
|
|
|
152
156
|
projectLocalSkills: null,
|
|
153
157
|
sandboxMode: settings.sandboxMode,
|
|
154
158
|
approvalPolicy: settings.approvalPolicy,
|
|
159
|
+
providerLaunch,
|
|
155
160
|
env,
|
|
156
161
|
emit,
|
|
157
162
|
// While subagent workers are active, the parent idle watchdog resets
|
|
@@ -281,7 +286,7 @@ function buildCodexTurnPrompt(params = {}, mirror = {}, projectLocalSkills, turn
|
|
|
281
286
|
turnAttachments,
|
|
282
287
|
codexSkillInvocationContext
|
|
283
288
|
});
|
|
284
|
-
return [prompt.systemPrompt, prompt.userPrompt].filter(Boolean).join('\n\n');
|
|
289
|
+
return [prompt.systemPrompt, buildReadProgressRules(), prompt.userPrompt].filter(Boolean).join('\n\n');
|
|
285
290
|
}
|
|
286
291
|
|
|
287
292
|
function materializeTurnAttachments(attachments = [], workspacePath = '') {
|
|
@@ -585,7 +590,8 @@ function buildThreadResumeParams(input = {}) {
|
|
|
585
590
|
|
|
586
591
|
function buildCodexAppServerArgs(input = {}) {
|
|
587
592
|
const args = [
|
|
588
|
-
...buildCodexSpeedArgs(normalizeSpeedTier(input.speedTier))
|
|
593
|
+
...buildCodexSpeedArgs(normalizeSpeedTier(input.speedTier)),
|
|
594
|
+
...buildProviderConfigArgs(input.providerLaunch)
|
|
589
595
|
];
|
|
590
596
|
if (input.loadCodexLocalSkills === false) {
|
|
591
597
|
args.push('--disable', 'plugins');
|
|
@@ -764,18 +770,27 @@ function supportsReasoningSummary(model) {
|
|
|
764
770
|
return String(model || '').toLowerCase() !== 'gpt-5.3-codex-spark';
|
|
765
771
|
}
|
|
766
772
|
|
|
767
|
-
function runCodexAppServerSession(input) {
|
|
773
|
+
async function runCodexAppServerSession(input) {
|
|
774
|
+
const prepared = await prepareProviderLaunch(input.providerLaunch, { signal: input.signal });
|
|
775
|
+
try {
|
|
776
|
+
return await runCodexAppServerProcess({ ...input, providerLaunch: prepared.launch });
|
|
777
|
+
} finally {
|
|
778
|
+
await prepared.close();
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function runCodexAppServerProcess(input) {
|
|
768
783
|
return new Promise((resolve, reject) => {
|
|
769
784
|
if (input.signal?.aborted) {
|
|
770
785
|
reject(getAbortReason(input.signal));
|
|
771
786
|
return;
|
|
772
787
|
}
|
|
773
|
-
const childEnv = buildCodexHomeEnv(input.env || process.env, {
|
|
788
|
+
const childEnv = applyProviderEnvironment(buildCodexHomeEnv(input.env || process.env, {
|
|
774
789
|
loadCodexLocalSkills: input.loadCodexLocalSkills !== false,
|
|
775
790
|
loadCodexOverleafSkills: input.loadCodexOverleafSkills !== false,
|
|
776
791
|
installCodexOverleafSkillsTarget: input.installCodexOverleafSkillsTarget === true,
|
|
777
792
|
projectLocalSkills: input.projectLocalSkills || null
|
|
778
|
-
});
|
|
793
|
+
}), input.providerLaunch);
|
|
779
794
|
const codexCommand = resolveCodexCommand(childEnv);
|
|
780
795
|
if (!codexCommand) {
|
|
781
796
|
reject(new Error('Codex CLI was not found. Install Codex or make sure the `codex` command is available in your login shell.'));
|
|
@@ -795,6 +810,11 @@ function runCodexAppServerSession(input) {
|
|
|
795
810
|
let activeTurnId = '';
|
|
796
811
|
const assistantMessages = new Map();
|
|
797
812
|
const assistantMessageOrder = [];
|
|
813
|
+
const readProgressController = createReadProgressController({
|
|
814
|
+
input, request, fail,
|
|
815
|
+
getTurn: () => ({ threadId: activeThreadId, turnId: activeTurnId }),
|
|
816
|
+
emitEvent: (...args) => emitCodexEvent(input.emit, ...args)
|
|
817
|
+
});
|
|
798
818
|
let settled = false;
|
|
799
819
|
// Two-layer timeout strategy:
|
|
800
820
|
// 1. Optional absolute deadline (CODEX_OVERLEAF_CODEX_TIMEOUT_MS) —
|
|
@@ -890,6 +910,7 @@ function runCodexAppServerSession(input) {
|
|
|
890
910
|
|
|
891
911
|
const turnResponse = await startTurnWithSummaryFallback(activeThreadId);
|
|
892
912
|
activeTurnId = turnResponse?.turn?.id || '';
|
|
913
|
+
readProgressController.flush();
|
|
893
914
|
}
|
|
894
915
|
|
|
895
916
|
async function startTurnWithSummaryFallback(threadId) {
|
|
@@ -976,6 +997,8 @@ function runCodexAppServerSession(input) {
|
|
|
976
997
|
method: message.method,
|
|
977
998
|
params: message.params || {}
|
|
978
999
|
}, inferNotificationStatus(message));
|
|
1000
|
+
if (message.method === 'item/completed' &&
|
|
1001
|
+
!readProgressController.observe(message.params?.item || {})) return;
|
|
979
1002
|
if (message.method === 'turn/completed' && (!activeTurnId || message.params?.turn?.id === activeTurnId || message.params?.turnId === activeTurnId)) {
|
|
980
1003
|
succeed();
|
|
981
1004
|
}
|
|
@@ -1378,25 +1401,6 @@ function getAbortReason(signal) {
|
|
|
1378
1401
|
return error;
|
|
1379
1402
|
}
|
|
1380
1403
|
|
|
1381
|
-
function resolveCodexCommand(env = process.env) {
|
|
1382
|
-
if (
|
|
1383
|
-
env.CODEX_OVERLEAF_ENV_READY === '1' ||
|
|
1384
|
-
Object.prototype.hasOwnProperty.call(env, 'CODEX_OVERLEAF_CODEX_PATH')
|
|
1385
|
-
) {
|
|
1386
|
-
return env.CODEX_OVERLEAF_CODEX_PATH || '';
|
|
1387
|
-
}
|
|
1388
|
-
return 'codex';
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
function shouldUseShellForCommand(command, env = process.env) {
|
|
1392
|
-
const platform = env.CODEX_OVERLEAF_PLATFORM || process.platform;
|
|
1393
|
-
if (platform !== 'win32') {
|
|
1394
|
-
return false;
|
|
1395
|
-
}
|
|
1396
|
-
const text = String(command || '');
|
|
1397
|
-
return text === 'codex' || /\.(?:cmd|bat)$/i.test(text);
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
1404
|
function buildFinalAssistantMessage(messages = new Map(), order = []) {
|
|
1401
1405
|
const values = [];
|
|
1402
1406
|
const seenIds = new Set();
|
|
@@ -131,6 +131,7 @@ function buildManagedExtensionTree(options = {}) {
|
|
|
131
131
|
copyPackageTree(packageRoot, 'extension/assets', targetRoot, 'assets', { allowedFiles });
|
|
132
132
|
copyPackageTree(packageRoot, 'extension/src', targetRoot, 'runtime/src', { allowedFiles });
|
|
133
133
|
copyPackageTree(packageRoot, 'extension/styles', targetRoot, 'runtime/styles', { allowedFiles });
|
|
134
|
+
copyPackageTree(packageRoot, 'extension/vendor', targetRoot, 'runtime/vendor', { allowedFiles });
|
|
134
135
|
copyPackageFile(packageRoot, 'extension/runtime-manifest.json', targetRoot, 'runtime/runtime-manifest.json', allowedFiles);
|
|
135
136
|
fs.writeFileSync(path.join(targetRoot, EXTENSION_MARKER), JSON.stringify({
|
|
136
137
|
managedBy: MANAGED_BY,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function classifyResponsesRoute(method, pathname) {
|
|
4
|
+
if (String(method || '').toUpperCase() !== 'POST') return '';
|
|
5
|
+
const path = String(pathname || '');
|
|
6
|
+
if (/\/responses\/compact\/?$/.test(path)) return 'compact';
|
|
7
|
+
if (/\/responses\/?$/.test(path)) return 'responses';
|
|
8
|
+
return '';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
module.exports = { classifyResponsesRoute };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_CONTEXT_WINDOW = 262144;
|
|
8
|
+
|
|
9
|
+
function buildProviderModelCatalogData(launch = {}) {
|
|
10
|
+
const configured = Array.isArray(launch.models) ? launch.models : [];
|
|
11
|
+
const fallbackId = String(launch.modelId || '').trim();
|
|
12
|
+
const models = configured.length > 0
|
|
13
|
+
? configured
|
|
14
|
+
: fallbackId
|
|
15
|
+
? [{ id: fallbackId, label: fallbackId }]
|
|
16
|
+
: [];
|
|
17
|
+
return {
|
|
18
|
+
models: models
|
|
19
|
+
.filter(model => model && typeof model.id === 'string' && model.id.trim())
|
|
20
|
+
.map((model, index) => buildModelEntry(model, launch, index))
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function prepareProviderModelCatalog(launch) {
|
|
25
|
+
if (!launch) {
|
|
26
|
+
return { launch, close() {} };
|
|
27
|
+
}
|
|
28
|
+
const catalog = buildProviderModelCatalogData(launch);
|
|
29
|
+
if (!catalog.models.length) {
|
|
30
|
+
return { launch, close() {} };
|
|
31
|
+
}
|
|
32
|
+
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-overleaf-models-'));
|
|
33
|
+
const modelCatalogPath = path.join(directory, 'models.json');
|
|
34
|
+
fs.writeFileSync(modelCatalogPath, `${JSON.stringify(catalog, null, 2)}\n`, { mode: 0o600 });
|
|
35
|
+
return {
|
|
36
|
+
launch: Object.freeze({ ...launch, modelCatalogPath }),
|
|
37
|
+
close() {
|
|
38
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function buildModelEntry(model, launch, index) {
|
|
44
|
+
const id = String(model.id || '').trim();
|
|
45
|
+
const efforts = normalizeEfforts(model.reasoningEfforts, launch.reasoningEfforts);
|
|
46
|
+
const defaultReasoning = efforts.find(effort => effort !== 'none') || efforts[0] || 'none';
|
|
47
|
+
const contextWindow = normalizeContextWindow(model.contextWindow || launch.contextWindow);
|
|
48
|
+
return {
|
|
49
|
+
slug: id,
|
|
50
|
+
display_name: String(model.label || id),
|
|
51
|
+
description: `Custom provider model ${id}`,
|
|
52
|
+
base_instructions: String(model.baseInstructions || launch.baseInstructions || (
|
|
53
|
+
'You are a coding agent. Use the available shell tool to inspect and modify files, follow the user request precisely, and report the result concisely.'
|
|
54
|
+
)),
|
|
55
|
+
default_reasoning_level: defaultReasoning,
|
|
56
|
+
supported_reasoning_levels: efforts.map(effort => ({
|
|
57
|
+
effort,
|
|
58
|
+
description: effort === 'none' ? 'Reasoning disabled' : `${effort} reasoning effort`
|
|
59
|
+
})),
|
|
60
|
+
shell_type: 'shell_command',
|
|
61
|
+
visibility: 'list',
|
|
62
|
+
supported_in_api: true,
|
|
63
|
+
priority: 10000 - index,
|
|
64
|
+
supports_reasoning_summaries: efforts.some(effort => effort !== 'none'),
|
|
65
|
+
default_reasoning_summary: 'none',
|
|
66
|
+
support_verbosity: false,
|
|
67
|
+
truncation_policy: { mode: 'bytes', limit: 10000 },
|
|
68
|
+
supports_parallel_tool_calls: Boolean(
|
|
69
|
+
model.supportsParallelToolCalls ?? launch.supportsParallelToolCalls
|
|
70
|
+
),
|
|
71
|
+
supports_image_detail_original: false,
|
|
72
|
+
context_window: contextWindow,
|
|
73
|
+
max_context_window: contextWindow,
|
|
74
|
+
effective_context_window_percent: 95,
|
|
75
|
+
experimental_supported_tools: [],
|
|
76
|
+
input_modalities: normalizeInputModalities(model.inputModalities || launch.inputModalities),
|
|
77
|
+
supports_search_tool: false
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeEfforts(...values) {
|
|
82
|
+
const allowed = new Set(['none', 'minimal', 'low', 'medium', 'high', 'xhigh']);
|
|
83
|
+
const normalized = values
|
|
84
|
+
.flatMap(value => Array.isArray(value) ? value : [])
|
|
85
|
+
.map(value => String(value || '').trim().toLowerCase())
|
|
86
|
+
.filter(value => allowed.has(value));
|
|
87
|
+
return Array.from(new Set(normalized.length ? normalized : ['none']));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function normalizeContextWindow(value) {
|
|
91
|
+
const parsed = Number.parseInt(String(value ?? ''), 10);
|
|
92
|
+
if (!Number.isFinite(parsed)) return DEFAULT_CONTEXT_WINDOW;
|
|
93
|
+
return Math.min(4000000, Math.max(8192, parsed));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function normalizeInputModalities(values) {
|
|
97
|
+
const normalized = (Array.isArray(values) ? values : ['text'])
|
|
98
|
+
.map(value => String(value || '').trim().toLowerCase())
|
|
99
|
+
.filter(value => value === 'text' || value === 'image');
|
|
100
|
+
const unique = Array.from(new Set(normalized));
|
|
101
|
+
return unique.includes('text') ? unique : ['text', ...unique];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
buildProviderModelCatalogData,
|
|
106
|
+
prepareProviderModelCatalog
|
|
107
|
+
};
|