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.
Files changed (122) hide show
  1. package/README.md +24 -23
  2. package/extension/bootstrap/manifest.template.json +2 -1
  3. package/extension/runtime-manifest.json +13 -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 +130 -65
  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 +981 -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/trackedChangesLifecycle.js +154 -26
  23. package/extension/src/page/writebackRouter.js +31 -60
  24. package/extension/src/pageBridge.js +1 -1
  25. package/extension/src/shared/agentTranscript.js +130 -0
  26. package/extension/src/shared/compatibility.js +10 -7
  27. package/extension/src/shared/failureReasons.js +75 -0
  28. package/extension/src/shared/i18n.js +22 -0
  29. package/extension/src/shared/models.js +1 -0
  30. package/extension/src/shared/providerProfiles.js +283 -0
  31. package/extension/src/shared/sessionState.js +12 -2
  32. package/extension/src/shared/storageDb.js +27 -19
  33. package/extension/src/shared/storageRunActions.js +99 -0
  34. package/extension/styles/panel.css +587 -1
  35. package/extension/vendor/katex/LICENSE +21 -0
  36. package/extension/vendor/katex/VERSION +2 -0
  37. package/extension/vendor/katex/fonts/KaTeX_AMS-Regular.ttf +0 -0
  38. package/extension/vendor/katex/fonts/KaTeX_AMS-Regular.woff +0 -0
  39. package/extension/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  40. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  41. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  42. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  43. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  44. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  45. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  46. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  47. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  48. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  49. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  50. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  51. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  52. package/extension/vendor/katex/fonts/KaTeX_Main-Bold.ttf +0 -0
  53. package/extension/vendor/katex/fonts/KaTeX_Main-Bold.woff +0 -0
  54. package/extension/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  55. package/extension/vendor/katex/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  56. package/extension/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  57. package/extension/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  58. package/extension/vendor/katex/fonts/KaTeX_Main-Italic.ttf +0 -0
  59. package/extension/vendor/katex/fonts/KaTeX_Main-Italic.woff +0 -0
  60. package/extension/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  61. package/extension/vendor/katex/fonts/KaTeX_Main-Regular.ttf +0 -0
  62. package/extension/vendor/katex/fonts/KaTeX_Main-Regular.woff +0 -0
  63. package/extension/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  64. package/extension/vendor/katex/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  65. package/extension/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  66. package/extension/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  67. package/extension/vendor/katex/fonts/KaTeX_Math-Italic.ttf +0 -0
  68. package/extension/vendor/katex/fonts/KaTeX_Math-Italic.woff +0 -0
  69. package/extension/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  70. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  71. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  72. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  73. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  74. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  75. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  76. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  77. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  78. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  79. package/extension/vendor/katex/fonts/KaTeX_Script-Regular.ttf +0 -0
  80. package/extension/vendor/katex/fonts/KaTeX_Script-Regular.woff +0 -0
  81. package/extension/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  82. package/extension/vendor/katex/fonts/KaTeX_Size1-Regular.ttf +0 -0
  83. package/extension/vendor/katex/fonts/KaTeX_Size1-Regular.woff +0 -0
  84. package/extension/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  85. package/extension/vendor/katex/fonts/KaTeX_Size2-Regular.ttf +0 -0
  86. package/extension/vendor/katex/fonts/KaTeX_Size2-Regular.woff +0 -0
  87. package/extension/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  88. package/extension/vendor/katex/fonts/KaTeX_Size3-Regular.ttf +0 -0
  89. package/extension/vendor/katex/fonts/KaTeX_Size3-Regular.woff +0 -0
  90. package/extension/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  91. package/extension/vendor/katex/fonts/KaTeX_Size4-Regular.ttf +0 -0
  92. package/extension/vendor/katex/fonts/KaTeX_Size4-Regular.woff +0 -0
  93. package/extension/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  94. package/extension/vendor/katex/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  95. package/extension/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  96. package/extension/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  97. package/extension/vendor/katex/katex.min.css +1 -0
  98. package/extension/vendor/katex/katex.min.js +1 -0
  99. package/native-host/src/anthropicBridgeRequest.js +435 -0
  100. package/native-host/src/anthropicBridgeResponse.js +431 -0
  101. package/native-host/src/anthropicMessagesBridge.js +281 -0
  102. package/native-host/src/chatBridgeRequest.js +274 -0
  103. package/native-host/src/chatBridgeResponse.js +594 -0
  104. package/native-host/src/chatCompletionsBridge.js +485 -0
  105. package/native-host/src/codexCommand.js +25 -0
  106. package/native-host/src/codexPromptAssembly.js +9 -0
  107. package/native-host/src/codexProviderLaunch.js +213 -0
  108. package/native-host/src/codexProviderTest.js +196 -0
  109. package/native-host/src/codexSessionRunner.js +29 -25
  110. package/native-host/src/managedInstall.js +1 -0
  111. package/native-host/src/providerBridgeRoutes.js +11 -0
  112. package/native-host/src/providerModelCatalog.js +107 -0
  113. package/native-host/src/providerProfile.js +454 -0
  114. package/native-host/src/providerReasoning.js +235 -0
  115. package/native-host/src/providerRedaction.js +18 -0
  116. package/native-host/src/providerRuntime.js +365 -0
  117. package/native-host/src/providerStore.js +691 -0
  118. package/native-host/src/readProgressGuard.js +360 -0
  119. package/native-host/src/taskRunnerRuntime.js +40 -4
  120. package/native-host/src/updateArchive.js +1 -0
  121. package/native-host/src/updateTrust.js +2 -1
  122. package/package.json +2 -1
@@ -0,0 +1,454 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const {
5
+ normalizeReasoningAdapter,
6
+ normalizeReasoningCapability
7
+ } = require('./providerReasoning');
8
+
9
+ const ALLOWED_WIRE_APIS = new Set(['auto', 'responses', 'chat', 'anthropic']);
10
+ const ALLOWED_REASONING_EFFORTS = new Set(['none', 'minimal', 'low', 'medium', 'high', 'xhigh']);
11
+ const ALLOWED_UPSTREAM_RESPONSE_MODES = new Set(['auto', 'streaming', 'buffered']);
12
+ const ALLOWED_AUTH_MODES = new Set(['bearer', 'x-api-key', 'api-key', 'custom', 'none']);
13
+ const ALLOWED_INPUT_MODALITIES = new Set(['text', 'image']);
14
+ const DEFAULT_CONTEXT_WINDOW = 262144;
15
+ const UNSAFE_CLI_VALUE = /[\0\r\n"&|<>^%!`]/;
16
+
17
+ function normalizeProviderDraft(value = {}) {
18
+ const name = normalizeRequiredText(value.name, 'provider_name_invalid', 'Provider name is required.', 64);
19
+ assertCliSafe(name, 'provider_name_invalid', 'Provider name contains unsupported command characters.');
20
+ const baseUrl = normalizeBaseUrl(value.baseUrl);
21
+ const wireApiPreference = normalizeWireApi(value.wireApiPreference, true);
22
+ const models = normalizeModels(value.models);
23
+ if (!models.length) {
24
+ throw providerError('provider_model_not_configured', 'At least one model ID is required.');
25
+ }
26
+ const defaultModelId = normalizeRequiredText(
27
+ value.defaultModelId,
28
+ 'provider_model_not_configured',
29
+ 'A default model ID is required.',
30
+ 200
31
+ );
32
+ assertCliSafe(defaultModelId, 'provider_model_not_configured', 'Default model ID contains unsupported command characters.');
33
+ if (!models.some(model => model.id === defaultModelId)) {
34
+ throw providerError('provider_model_not_configured', 'The default model must be present in the provider model list.');
35
+ }
36
+ const requestTimeoutMs = normalizeTimeout(value.requestTimeoutMs);
37
+ const reasoningAdapter = normalizeReasoningAdapter(value.reasoningAdapter);
38
+ const reasoningCapability = normalizeReasoningCapability(value.reasoningCapability);
39
+ const authMode = normalizeEnum(value.authMode, ALLOWED_AUTH_MODES, 'bearer');
40
+ const apiKeyHeaderName = normalizeHeaderName(value.apiKeyHeaderName);
41
+ if (authMode === 'custom' && !apiKeyHeaderName) {
42
+ throw providerError('provider_configuration_invalid', 'Custom API-key authentication requires a header name.');
43
+ }
44
+ return {
45
+ name,
46
+ baseUrl,
47
+ wireApiPreference,
48
+ models,
49
+ defaultModelId,
50
+ requestTimeoutMs,
51
+ reasoningAdapter,
52
+ reasoningCapability,
53
+ authMode,
54
+ apiKeyHeaderName,
55
+ fullEndpoint: value.fullEndpoint === true,
56
+ customHeaders: normalizeStringRecord(value.customHeaders, 'Custom headers', { rejectCredentials: true }),
57
+ queryParams: normalizeStringRecord(value.queryParams, 'Query parameters', { rejectCredentials: true }),
58
+ bodyOverrides: normalizeBodyOverrides(value.bodyOverrides),
59
+ contextWindow: normalizeContextWindow(value.contextWindow),
60
+ supportsParallelToolCalls: value.supportsParallelToolCalls === true,
61
+ supportsStreamOptions: value.supportsStreamOptions === true,
62
+ inputModalities: normalizeInputModalities(value.inputModalities),
63
+ anthropicVersion: normalizeOptionalText(value.anthropicVersion, 64) || '2023-06-01',
64
+ anthropicBeta: normalizeOptionalText(value.anthropicBeta, 512),
65
+ anthropicThinkingMode: normalizeAnthropicThinkingMode(value.anthropicThinkingMode),
66
+ anthropicPromptCaching: value.anthropicPromptCaching === true,
67
+ impersonateClaudeCode: value.impersonateClaudeCode === true,
68
+ maxOutputTokens: normalizeMaxOutputTokens(value.maxOutputTokens)
69
+ };
70
+ }
71
+
72
+ function normalizeModels(values) {
73
+ if (!Array.isArray(values)) {
74
+ return [];
75
+ }
76
+ const seen = new Set();
77
+ const result = [];
78
+ for (const value of values.slice(0, 32)) {
79
+ const id = normalizeText(typeof value === 'string' ? value : value?.id);
80
+ if (!id || id.length > 200 || seen.has(id)) {
81
+ continue;
82
+ }
83
+ assertCliSafe(id, 'provider_model_not_configured', `Model ID ${id} contains unsupported command characters.`);
84
+ seen.add(id);
85
+ const label = normalizeText(value?.label) || id;
86
+ const reasoningEfforts = Array.isArray(value?.reasoningEfforts)
87
+ ? value.reasoningEfforts.map(normalizeText).filter(effort => ALLOWED_REASONING_EFFORTS.has(effort))
88
+ : [];
89
+ result.push({
90
+ id,
91
+ label: label.slice(0, 200),
92
+ reasoningEfforts: Array.from(new Set(reasoningEfforts)),
93
+ upstreamResponseMode: normalizeUpstreamResponseMode(value?.upstreamResponseMode, true),
94
+ resolvedUpstreamResponseMode: normalizeUpstreamResponseMode(value?.resolvedUpstreamResponseMode, false),
95
+ contextWindow: normalizeContextWindow(value?.contextWindow),
96
+ supportsParallelToolCalls: value?.supportsParallelToolCalls === true,
97
+ inputModalities: normalizeInputModalities(value?.inputModalities),
98
+ baseInstructions: normalizeOptionalText(value?.baseInstructions, 8000)
99
+ });
100
+ }
101
+ return result;
102
+ }
103
+
104
+ function normalizeBaseUrl(value) {
105
+ const text = normalizeRequiredText(value, 'provider_base_url_invalid', 'Base URL is required.', 2048);
106
+ let parsed;
107
+ try {
108
+ parsed = new URL(text);
109
+ } catch (_error) {
110
+ throw providerError('provider_base_url_invalid', 'Base URL is not a valid URL.');
111
+ }
112
+ if (parsed.username || parsed.password) {
113
+ throw providerError('provider_base_url_invalid', 'Base URL cannot contain credentials.');
114
+ }
115
+ if (parsed.search || parsed.hash) {
116
+ throw providerError('provider_base_url_invalid', 'Base URL cannot contain a query string or fragment.');
117
+ }
118
+ if (parsed.protocol !== 'https:' && !(parsed.protocol === 'http:' && isLoopbackHost(parsed.hostname))) {
119
+ throw providerError('provider_base_url_insecure', 'HTTPS is required except for loopback endpoints.');
120
+ }
121
+ const pathname = parsed.pathname.replace(/\/+$/, '');
122
+ const normalized = `${parsed.origin}${pathname === '/' ? '' : pathname}`;
123
+ assertCliSafe(normalized, 'provider_base_url_invalid', 'Base URL contains unsupported command characters.');
124
+ return normalized;
125
+ }
126
+
127
+ function normalizeWireApi(value, allowAuto = false) {
128
+ const normalized = normalizeText(value).toLowerCase();
129
+ if (ALLOWED_WIRE_APIS.has(normalized) && (allowAuto || normalized !== 'auto')) {
130
+ return normalized;
131
+ }
132
+ if (allowAuto && !normalized) {
133
+ return 'auto';
134
+ }
135
+ throw providerError('provider_protocol_incompatible', 'API protocol must be Auto, Responses, Chat Completions, or Anthropic Messages.');
136
+ }
137
+
138
+ function isResolvedWireApi(value) {
139
+ return ALLOWED_WIRE_APIS.has(String(value || '').trim().toLowerCase())
140
+ && String(value || '').trim().toLowerCase() !== 'auto';
141
+ }
142
+
143
+ function normalizeSecret(value) {
144
+ if (value === undefined || value === null || value === '') {
145
+ return '';
146
+ }
147
+ const text = String(value);
148
+ if (text.length > 8192 || /[\0\r\n]/.test(text)) {
149
+ throw providerError('provider_secret_invalid', 'API key contains unsupported characters or exceeds the size limit.');
150
+ }
151
+ return text;
152
+ }
153
+
154
+ function computeDraftFingerprint(draft, secret) {
155
+ const normalized = normalizeProviderDraft(draft);
156
+ const fingerprintDraft = {
157
+ ...normalized,
158
+ models: normalized.models.map(({ resolvedUpstreamResponseMode: _resolvedMode, ...model }) => model)
159
+ };
160
+ const secretDigest = crypto.createHash('sha256').update(normalizeSecret(secret)).digest('hex');
161
+ return crypto.createHash('sha256').update(JSON.stringify({ normalized: fingerprintDraft, secretDigest })).digest('hex');
162
+ }
163
+
164
+ function computeConnectionFingerprint(draft, secret, modelId) {
165
+ const normalized = normalizeProviderDraft(draft);
166
+ const selectedModelId = normalizeText(modelId) || normalized.defaultModelId;
167
+ const selectedModel = normalized.models.find(model => model.id === selectedModelId);
168
+ if (!selectedModel) {
169
+ throw providerError('provider_model_not_configured', 'The selected model is not configured for this provider.');
170
+ }
171
+ const { resolvedUpstreamResponseMode: _resolvedMode, ...model } = selectedModel;
172
+ const connection = {
173
+ baseUrl: normalized.baseUrl,
174
+ wireApiPreference: normalized.wireApiPreference,
175
+ model,
176
+ requestTimeoutMs: normalized.requestTimeoutMs,
177
+ reasoningAdapter: normalized.reasoningAdapter,
178
+ reasoningCapability: normalized.reasoningCapability,
179
+ authMode: normalized.authMode,
180
+ apiKeyHeaderName: normalized.apiKeyHeaderName,
181
+ fullEndpoint: normalized.fullEndpoint,
182
+ customHeaders: normalized.customHeaders,
183
+ queryParams: normalized.queryParams,
184
+ bodyOverrides: normalized.bodyOverrides,
185
+ supportsParallelToolCalls: normalized.supportsParallelToolCalls,
186
+ supportsStreamOptions: normalized.supportsStreamOptions,
187
+ anthropicVersion: normalized.anthropicVersion,
188
+ anthropicBeta: normalized.anthropicBeta,
189
+ anthropicThinkingMode: normalized.anthropicThinkingMode,
190
+ anthropicPromptCaching: normalized.anthropicPromptCaching,
191
+ impersonateClaudeCode: normalized.impersonateClaudeCode,
192
+ maxOutputTokens: normalized.maxOutputTokens
193
+ };
194
+ const secretDigest = crypto.createHash('sha256').update(normalizeSecret(secret)).digest('hex');
195
+ return crypto.createHash('sha256').update(JSON.stringify({ connection, secretDigest })).digest('hex');
196
+ }
197
+
198
+ function getEndpointHost(baseUrl) {
199
+ try {
200
+ return new URL(baseUrl).hostname;
201
+ } catch (_error) {
202
+ return '';
203
+ }
204
+ }
205
+
206
+ function sanitizeProfile(profile, hasSecret) {
207
+ return {
208
+ id: profile.id,
209
+ kind: 'custom',
210
+ revision: profile.revision,
211
+ name: profile.name,
212
+ baseUrl: profile.baseUrl,
213
+ wireApiPreference: profile.wireApiPreference,
214
+ resolvedWireApi: profile.resolvedWireApi || '',
215
+ models: profile.models.map(model => ({ ...model, reasoningEfforts: model.reasoningEfforts.slice() })),
216
+ defaultModelId: profile.defaultModelId,
217
+ requestTimeoutMs: profile.requestTimeoutMs,
218
+ reasoningAdapter: profile.reasoningAdapter || 'auto',
219
+ reasoningCapability: profile.reasoningCapability || 'auto',
220
+ authMode: profile.authMode || 'bearer',
221
+ apiKeyHeaderName: profile.apiKeyHeaderName || '',
222
+ fullEndpoint: profile.fullEndpoint === true,
223
+ customHeaders: { ...(profile.customHeaders || {}) },
224
+ queryParams: { ...(profile.queryParams || {}) },
225
+ bodyOverrides: { ...(profile.bodyOverrides || {}) },
226
+ contextWindow: normalizeContextWindow(profile.contextWindow),
227
+ supportsParallelToolCalls: profile.supportsParallelToolCalls === true,
228
+ supportsStreamOptions: profile.supportsStreamOptions === true,
229
+ inputModalities: normalizeInputModalities(profile.inputModalities),
230
+ anthropicVersion: profile.anthropicVersion || '2023-06-01',
231
+ anthropicBeta: profile.anthropicBeta || '',
232
+ anthropicThinkingMode: normalizeAnthropicThinkingMode(profile.anthropicThinkingMode),
233
+ anthropicPromptCaching: profile.anthropicPromptCaching === true,
234
+ impersonateClaudeCode: profile.impersonateClaudeCode === true,
235
+ maxOutputTokens: normalizeMaxOutputTokens(profile.maxOutputTokens),
236
+ hasSecret: Boolean(hasSecret),
237
+ secretUpdatedAt: profile.secretUpdatedAt || 0,
238
+ endpointDisclosureHost: profile.endpointDisclosureHost || '',
239
+ endpointDisclosureBaseUrl: profile.endpointDisclosureBaseUrl || '',
240
+ endpointDisclosureAcceptedAt: profile.endpointDisclosureAcceptedAt || 0,
241
+ modelDiagnostics: Object.fromEntries(Object.entries(profile.modelDiagnostics || {}).map(([modelId, diagnostic]) => [
242
+ modelId,
243
+ {
244
+ modelId,
245
+ status: diagnostic?.status === 'failed' ? 'failed' : 'tested',
246
+ at: diagnostic?.at || 0,
247
+ wireApi: diagnostic?.wireApi || '',
248
+ upstreamResponseMode: normalizeUpstreamResponseMode(diagnostic?.upstreamResponseMode, false),
249
+ durationMs: diagnostic?.durationMs || 0,
250
+ errorCode: diagnostic?.errorCode || ''
251
+ }
252
+ ])),
253
+ lastVerified: profile.lastVerified
254
+ ? {
255
+ revision: profile.lastVerified.revision,
256
+ at: profile.lastVerified.at,
257
+ modelId: profile.lastVerified.modelId,
258
+ wireApi: profile.lastVerified.wireApi,
259
+ upstreamResponseMode: normalizeUpstreamResponseMode(profile.lastVerified.upstreamResponseMode, false)
260
+ }
261
+ : null,
262
+ createdAt: profile.createdAt || 0,
263
+ updatedAt: profile.updatedAt || 0
264
+ };
265
+ }
266
+
267
+ function normalizeTimeout(value) {
268
+ const number = Number(value);
269
+ if (!Number.isFinite(number)) {
270
+ return 30000;
271
+ }
272
+ return Math.min(120000, Math.max(5000, Math.floor(number)));
273
+ }
274
+
275
+ function normalizeEnum(value, allowed, fallback) {
276
+ const normalized = normalizeText(value).toLowerCase();
277
+ return allowed.has(normalized) ? normalized : fallback;
278
+ }
279
+
280
+ function normalizeHeaderName(value) {
281
+ const normalized = normalizeText(value);
282
+ if (!normalized) return '';
283
+ if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(normalized)) {
284
+ throw providerError('provider_configuration_invalid', 'Custom API-key header name is invalid.');
285
+ }
286
+ return normalized;
287
+ }
288
+
289
+ function parseJsonObject(value, label) {
290
+ if (value === undefined || value === null || value === '') return {};
291
+ let parsed = value;
292
+ if (typeof value === 'string') {
293
+ try {
294
+ parsed = JSON.parse(value);
295
+ } catch (_error) {
296
+ throw providerError('provider_configuration_invalid', `${label} must be valid JSON.`);
297
+ }
298
+ }
299
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
300
+ throw providerError('provider_configuration_invalid', `${label} must be a JSON object.`);
301
+ }
302
+ return parsed;
303
+ }
304
+
305
+ function normalizeStringRecord(value, label, options = {}) {
306
+ const parsed = parseJsonObject(value, label);
307
+ const result = {};
308
+ for (const [rawKey, rawValue] of Object.entries(parsed)) {
309
+ const key = normalizeText(rawKey);
310
+ const item = String(rawValue ?? '');
311
+ if (!key || /[\0\r\n]/.test(key) || /[\0\r\n]/.test(item)) {
312
+ throw providerError('provider_configuration_invalid', `${label} contains an invalid key or value.`);
313
+ }
314
+ if (options.rejectCredentials && isCredentialLikeKey(key)) {
315
+ throw providerError(
316
+ 'provider_configuration_invalid',
317
+ `${label} cannot contain credentials. Use the API-key authentication control instead.`
318
+ );
319
+ }
320
+ result[key] = item;
321
+ }
322
+ return result;
323
+ }
324
+
325
+ function normalizeBodyOverrides(value) {
326
+ const parsed = parseJsonObject(value, 'Request body overrides');
327
+ for (const blocked of ['model', 'messages', 'tools', 'stream', 'system', 'max_tokens', 'input', 'instructions']) {
328
+ if (Object.prototype.hasOwnProperty.call(parsed, blocked)) {
329
+ throw providerError('provider_configuration_invalid', `Request body overrides cannot replace ${blocked}.`);
330
+ }
331
+ }
332
+ if (JSON.stringify(parsed).length > 16384) {
333
+ throw providerError('provider_configuration_invalid', 'Request body overrides are too large.');
334
+ }
335
+ const credentialPath = findCredentialField(parsed);
336
+ if (credentialPath) {
337
+ throw providerError(
338
+ 'provider_configuration_invalid',
339
+ `Request body overrides cannot contain credential field ${credentialPath}. Use the API-key authentication control instead.`
340
+ );
341
+ }
342
+ return parsed;
343
+ }
344
+
345
+ function findCredentialField(value, path = []) {
346
+ if (Array.isArray(value)) {
347
+ for (let index = 0; index < value.length; index += 1) {
348
+ const found = findCredentialField(value[index], [...path, String(index)]);
349
+ if (found) return found;
350
+ }
351
+ return '';
352
+ }
353
+ if (!value || typeof value !== 'object') return '';
354
+ for (const [key, item] of Object.entries(value)) {
355
+ const nextPath = [...path, key];
356
+ if (isCredentialLikeKey(key)) return nextPath.join('.');
357
+ const found = findCredentialField(item, nextPath);
358
+ if (found) return found;
359
+ }
360
+ return '';
361
+ }
362
+
363
+ function isCredentialLikeKey(value) {
364
+ const key = String(value || '').trim();
365
+ if (!key) return false;
366
+ if (/^(?:prompt[-_.]?cache[-_.]?key|idempotency[-_.]?key|cache[-_.]?key)$/i.test(key)) {
367
+ return false;
368
+ }
369
+ if (/^(?:authorization|proxy-authorization|cookie|set-cookie|password|passwd|credential|credentials|token|key|signature|sig)$/i.test(key)) {
370
+ return true;
371
+ }
372
+ return /(?:^|[-_.])(?:api[-_]?key|access[-_]?token|auth[-_]?token|bearer[-_]?token|client[-_]?secret|secret[-_]?key|private[-_]?key|access[-_]?key(?:[-_]?id)?|token|secret|password|passwd|credential|credentials|cookie|signature)(?:$|[-_.])/i.test(key);
373
+ }
374
+
375
+ function normalizeContextWindow(value) {
376
+ const number = Number(value);
377
+ if (!Number.isFinite(number)) return DEFAULT_CONTEXT_WINDOW;
378
+ return Math.min(4000000, Math.max(8192, Math.floor(number)));
379
+ }
380
+
381
+ function normalizeInputModalities(value) {
382
+ const values = Array.isArray(value) ? value : normalizeText(value).split(',');
383
+ const normalized = values
384
+ .map(item => normalizeText(item).toLowerCase())
385
+ .filter(item => ALLOWED_INPUT_MODALITIES.has(item));
386
+ const result = Array.from(new Set(normalized));
387
+ return result.includes('text') ? result : ['text', ...result];
388
+ }
389
+
390
+ function normalizeOptionalText(value, maxLength) {
391
+ const text = normalizeText(value);
392
+ return text ? text.slice(0, maxLength) : '';
393
+ }
394
+
395
+ function normalizeAnthropicThinkingMode(value) {
396
+ const normalized = normalizeText(value).toLowerCase();
397
+ return ['budget', 'adaptive', 'none'].includes(normalized) ? normalized : 'budget';
398
+ }
399
+
400
+ function normalizeUpstreamResponseMode(value, allowAuto) {
401
+ const normalized = normalizeText(value).toLowerCase();
402
+ if (ALLOWED_UPSTREAM_RESPONSE_MODES.has(normalized) && (allowAuto || normalized !== 'auto')) {
403
+ return normalized;
404
+ }
405
+ return allowAuto ? 'auto' : '';
406
+ }
407
+
408
+ function normalizeMaxOutputTokens(value) {
409
+ const number = Number(value);
410
+ if (!Number.isFinite(number) || number === 8192) return 32768;
411
+ return Math.min(200000, Math.max(256, Math.floor(number)));
412
+ }
413
+
414
+ function normalizeRequiredText(value, code, message, maxLength) {
415
+ const text = normalizeText(value);
416
+ if (!text || text.length > maxLength || /[\0\r\n]/.test(text)) {
417
+ throw providerError(code, message);
418
+ }
419
+ return text;
420
+ }
421
+
422
+ function normalizeText(value) {
423
+ return typeof value === 'string' ? value.trim() : '';
424
+ }
425
+
426
+ function assertCliSafe(value, code, message) {
427
+ if (UNSAFE_CLI_VALUE.test(value)) {
428
+ throw providerError(code, message);
429
+ }
430
+ }
431
+
432
+ function isLoopbackHost(hostname) {
433
+ const host = String(hostname || '').toLowerCase().replace(/^\[|\]$/g, '');
434
+ return host === 'localhost' || host.endsWith('.localhost') || host === '::1' || /^127(?:\.\d{1,3}){3}$/.test(host);
435
+ }
436
+
437
+ function providerError(code, message, details = {}) {
438
+ const error = new Error(message);
439
+ error.code = code;
440
+ Object.assign(error, details);
441
+ return error;
442
+ }
443
+
444
+ module.exports = {
445
+ computeConnectionFingerprint,
446
+ computeDraftFingerprint,
447
+ getEndpointHost,
448
+ isResolvedWireApi,
449
+ normalizeProviderDraft,
450
+ normalizeSecret,
451
+ normalizeWireApi,
452
+ providerError,
453
+ sanitizeProfile
454
+ };
@@ -0,0 +1,235 @@
1
+ 'use strict';
2
+
3
+ const REASONING_ADAPTERS = new Set([
4
+ 'auto',
5
+ 'none',
6
+ 'deepseek',
7
+ 'anthropic',
8
+ 'reasoning_effort',
9
+ 'openrouter',
10
+ 'enable_thinking',
11
+ 'thinking',
12
+ 'reasoning_split'
13
+ ]);
14
+ const REASONING_CAPABILITIES = new Set(['auto', 'none', 'toggle', 'effort']);
15
+ const REASONING_EFFORTS = new Set(['none', 'minimal', 'low', 'medium', 'high', 'xhigh']);
16
+ const DEEPSEEK_MAX_REASONING_OUTPUT_TOKENS = 65536;
17
+
18
+ function normalizeReasoningAdapter(value) {
19
+ const normalized = String(value || '').trim().toLowerCase();
20
+ return REASONING_ADAPTERS.has(normalized) ? normalized : 'auto';
21
+ }
22
+
23
+ function normalizeReasoningCapability(value) {
24
+ const normalized = String(value || '').trim().toLowerCase();
25
+ return REASONING_CAPABILITIES.has(normalized) ? normalized : 'auto';
26
+ }
27
+
28
+ function resolveReasoningAdapter(profile = {}, modelId = '') {
29
+ const configured = normalizeReasoningAdapter(profile.reasoningAdapter);
30
+ if (configured !== 'auto') {
31
+ return configured;
32
+ }
33
+ const capability = normalizeReasoningCapability(profile.reasoningCapability);
34
+ if (capability === 'none') {
35
+ return 'none';
36
+ }
37
+ const detected = detectReasoningAdapter(profile, modelId);
38
+ if (detected) {
39
+ return detected;
40
+ }
41
+ if (capability === 'effort') {
42
+ return 'reasoning_effort';
43
+ }
44
+ if (capability === 'toggle') {
45
+ return 'enable_thinking';
46
+ }
47
+ return 'none';
48
+ }
49
+
50
+ function resolveReasoningCapability(profile = {}, modelId = '') {
51
+ const configured = normalizeReasoningCapability(profile.reasoningCapability);
52
+ if (configured !== 'auto') {
53
+ return configured;
54
+ }
55
+ const adapter = resolveReasoningAdapter(profile, modelId);
56
+ if (adapter === 'deepseek' || adapter === 'anthropic' || adapter === 'reasoning_effort' || adapter === 'openrouter') {
57
+ return 'effort';
58
+ }
59
+ if (adapter === 'enable_thinking' || adapter === 'thinking' || adapter === 'reasoning_split') {
60
+ return 'toggle';
61
+ }
62
+ return 'none';
63
+ }
64
+
65
+ function getSupportedReasoningEfforts(profile = {}, model = {}) {
66
+ return getReasoningControl(profile, model).efforts;
67
+ }
68
+
69
+ function getReasoningControl(profile = {}, model = {}) {
70
+ const explicit = Array.isArray(model.reasoningEfforts)
71
+ ? model.reasoningEfforts.map(normalizeEffort).filter(Boolean)
72
+ : [];
73
+ const adapter = resolveReasoningAdapter(profile, model.id);
74
+ const capability = resolveReasoningCapability(profile, model.id);
75
+ if (adapter === 'none' || capability === 'none') {
76
+ return { efforts: [], defaultEffort: '', presentation: 'none' };
77
+ }
78
+ if (explicit.length) {
79
+ const efforts = Array.from(new Set(explicit));
80
+ return {
81
+ efforts,
82
+ defaultEffort: efforts.includes('high') ? 'high' : efforts[0],
83
+ presentation: adapter === 'deepseek' ? 'deepseek' : capability === 'toggle' ? 'toggle' : 'effort'
84
+ };
85
+ }
86
+ if (adapter === 'deepseek') {
87
+ return {
88
+ efforts: ['none', 'high', 'xhigh'],
89
+ defaultEffort: 'high',
90
+ presentation: 'deepseek'
91
+ };
92
+ }
93
+ if (adapter === 'openrouter') {
94
+ return {
95
+ efforts: ['none', 'low', 'medium', 'high', 'xhigh'],
96
+ defaultEffort: 'medium',
97
+ presentation: 'effort'
98
+ };
99
+ }
100
+ if (adapter === 'anthropic') {
101
+ return {
102
+ efforts: ['none', 'low', 'medium', 'high', 'xhigh'],
103
+ defaultEffort: 'medium',
104
+ presentation: 'effort'
105
+ };
106
+ }
107
+ if (capability === 'effort') {
108
+ return {
109
+ efforts: ['low', 'medium', 'high', 'xhigh'],
110
+ defaultEffort: 'medium',
111
+ presentation: 'effort'
112
+ };
113
+ }
114
+ if (capability === 'toggle') {
115
+ return {
116
+ efforts: ['none', 'high'],
117
+ defaultEffort: 'high',
118
+ presentation: 'toggle'
119
+ };
120
+ }
121
+ return { efforts: [], defaultEffort: '', presentation: 'none' };
122
+ }
123
+
124
+ function applyReasoningControl(chatBody, responsesRequest = {}, launch = {}) {
125
+ // The panel selection is the user's explicit per-run choice. Codex may put
126
+ // its own model default in the Responses request, so launch must win when
127
+ // it is present (including the meaningful `none` value).
128
+ const requested = resolveRequestedReasoningEffort(responsesRequest, launch);
129
+ if (!requested) {
130
+ return;
131
+ }
132
+ const adapter = resolveReasoningAdapter(launch, chatBody.model || launch.modelId);
133
+ const enabled = requested !== 'none';
134
+ if (adapter === 'deepseek') {
135
+ chatBody.thinking = { type: enabled ? 'enabled' : 'disabled' };
136
+ if (enabled) {
137
+ chatBody.reasoning_effort = requested === 'xhigh' ? 'max' : 'high';
138
+ }
139
+ } else if (adapter === 'reasoning_effort') {
140
+ if (enabled) {
141
+ chatBody.reasoning_effort = clampEffort(requested, false);
142
+ }
143
+ } else if (adapter === 'openrouter') {
144
+ chatBody.reasoning = { effort: clampEffort(requested, true) };
145
+ } else if (adapter === 'enable_thinking') {
146
+ chatBody.enable_thinking = enabled;
147
+ } else if (adapter === 'thinking') {
148
+ chatBody.thinking = { type: enabled ? 'enabled' : 'disabled' };
149
+ } else if (adapter === 'reasoning_split') {
150
+ chatBody.reasoning_split = enabled;
151
+ }
152
+ }
153
+
154
+ function resolveRequestedReasoningEffort(responsesRequest = {}, launch = {}) {
155
+ return normalizeEffort(launch.reasoningEffort)
156
+ || normalizeEffort(responsesRequest?.reasoning?.effort);
157
+ }
158
+
159
+ function resolveReasoningMaxOutputTokens(value, responsesRequest = {}, launch = {}, modelId = '') {
160
+ const number = Number(value);
161
+ if (!Number.isFinite(number)) {
162
+ return value;
163
+ }
164
+ const normalized = Math.max(1, Math.floor(number));
165
+ const effort = resolveRequestedReasoningEffort(responsesRequest, launch);
166
+ const adapter = resolveReasoningAdapter(launch, modelId || launch.modelId);
167
+ return adapter === 'deepseek' && effort === 'xhigh'
168
+ ? Math.max(normalized, DEEPSEEK_MAX_REASONING_OUTPUT_TOKENS)
169
+ : normalized;
170
+ }
171
+
172
+ function requiresReasoningContentReplay(launch = {}, modelId = '') {
173
+ return resolveReasoningAdapter(launch, modelId) === 'deepseek';
174
+ }
175
+
176
+ function supportsToolChoice(launch = {}, modelId = '') {
177
+ return resolveReasoningAdapter(launch, modelId) !== 'deepseek';
178
+ }
179
+
180
+ function detectReasoningAdapter(profile = {}, modelId = '') {
181
+ if (profile.wireApiPreference === 'anthropic' || profile.wireApi === 'anthropic' || profile.routedWireApi === 'anthropic') {
182
+ return 'anthropic';
183
+ }
184
+ const host = normalizeHost(profile.baseUrl);
185
+ if (host === 'openrouter.ai' || host.endsWith('.openrouter.ai')) {
186
+ return 'openrouter';
187
+ }
188
+ if (host === 'api.deepseek.com' || host.endsWith('.deepseek.com')) {
189
+ return 'deepseek';
190
+ }
191
+ if (host === 'api.anthropic.com' || host.endsWith('.anthropic.com')) {
192
+ return 'anthropic';
193
+ }
194
+ if (host.includes('dashscope') || host.includes('bailian')) {
195
+ return 'enable_thinking';
196
+ }
197
+ if (host.includes('bigmodel') || host.includes('zhipu')) {
198
+ return 'thinking';
199
+ }
200
+ const identity = `${profile.providerName || profile.name || ''} ${modelId || profile.modelId || ''}`.toLowerCase();
201
+ if (/\bdeepseek(?:[-_\s]|$)/.test(identity)) return 'deepseek';
202
+ return /\b(?:anthropic|claude)(?:[-_\s]|$)/.test(identity) ? 'anthropic' : '';
203
+ }
204
+
205
+ function normalizeHost(value) {
206
+ try {
207
+ return new URL(String(value || '')).hostname.toLowerCase();
208
+ } catch (_error) {
209
+ return '';
210
+ }
211
+ }
212
+
213
+ function normalizeEffort(value) {
214
+ const normalized = String(value || '').trim().toLowerCase();
215
+ return REASONING_EFFORTS.has(normalized) ? normalized : '';
216
+ }
217
+
218
+ function clampEffort(effort, preserveXhigh) {
219
+ if (effort === 'minimal') return 'low';
220
+ if (effort === 'xhigh') return preserveXhigh ? 'xhigh' : 'high';
221
+ return effort;
222
+ }
223
+
224
+ module.exports = {
225
+ applyReasoningControl,
226
+ getReasoningControl,
227
+ getSupportedReasoningEfforts,
228
+ normalizeReasoningAdapter,
229
+ normalizeReasoningCapability,
230
+ requiresReasoningContentReplay,
231
+ resolveReasoningAdapter,
232
+ resolveReasoningCapability,
233
+ resolveReasoningMaxOutputTokens,
234
+ supportsToolChoice
235
+ };