@adversity/coding-tool-x 3.1.0 → 3.1.2

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 (142) hide show
  1. package/CHANGELOG.md +39 -18
  2. package/README.md +8 -8
  3. package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
  4. package/dist/web/assets/ConfigTemplates-DvcbKKdS.js +1 -0
  5. package/dist/web/assets/Home-BJKPCBuk.css +1 -0
  6. package/dist/web/assets/Home-Cw-F_Wnu.js +1 -0
  7. package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
  8. package/dist/web/assets/PluginManager-jy_4GVxI.js +1 -0
  9. package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
  10. package/dist/web/assets/ProjectList-Df1-NcNr.js +1 -0
  11. package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
  12. package/dist/web/assets/SessionList-UWcZtC2r.js +1 -0
  13. package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
  14. package/dist/web/assets/SkillManager-IRdseMKB.js +1 -0
  15. package/dist/web/assets/Terminal-BasTyDut.js +1 -0
  16. package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
  17. package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
  18. package/dist/web/assets/WorkspaceManager-D-D2kK1V.js +1 -0
  19. package/dist/web/assets/icons-kcfLIMBB.js +1 -0
  20. package/dist/web/assets/index-CoB3zF0K.css +1 -0
  21. package/dist/web/assets/index-CryrSLv8.js +2 -0
  22. package/dist/web/assets/markdown-BfC0goYb.css +10 -0
  23. package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
  24. package/dist/web/assets/naive-ui-CSrLusZZ.js +1 -0
  25. package/dist/web/assets/{vendors-D2HHw_aW.js → vendors-CO3Upi1d.js} +2 -2
  26. package/dist/web/assets/vue-vendor-DqyWIXEb.js +45 -0
  27. package/dist/web/assets/xterm-6GBZ9nXN.css +32 -0
  28. package/dist/web/assets/xterm-BJzAjXCH.js +13 -0
  29. package/dist/web/index.html +8 -6
  30. package/package.json +4 -2
  31. package/src/commands/channels.js +48 -1
  32. package/src/commands/cli-type.js +4 -2
  33. package/src/commands/daemon.js +81 -12
  34. package/src/commands/doctor.js +10 -9
  35. package/src/commands/list.js +1 -1
  36. package/src/commands/logs.js +6 -4
  37. package/src/commands/port-config.js +24 -4
  38. package/src/commands/proxy-control.js +12 -6
  39. package/src/commands/search.js +1 -1
  40. package/src/commands/security.js +3 -2
  41. package/src/commands/stats.js +226 -52
  42. package/src/commands/switch.js +1 -1
  43. package/src/commands/toggle-proxy.js +31 -6
  44. package/src/commands/update.js +97 -0
  45. package/src/commands/workspace.js +1 -1
  46. package/src/config/default.js +41 -2
  47. package/src/config/loader.js +74 -8
  48. package/src/config/model-metadata.js +415 -0
  49. package/src/config/model-pricing.js +23 -93
  50. package/src/config/paths.js +105 -33
  51. package/src/index.js +64 -3
  52. package/src/plugins/constants.js +3 -2
  53. package/src/plugins/plugin-api.js +1 -1
  54. package/src/reset-config.js +4 -2
  55. package/src/server/api/agents.js +57 -14
  56. package/src/server/api/channels.js +112 -33
  57. package/src/server/api/codex-channels.js +111 -18
  58. package/src/server/api/codex-proxy.js +14 -8
  59. package/src/server/api/commands.js +71 -18
  60. package/src/server/api/config-export.js +0 -6
  61. package/src/server/api/config-registry.js +11 -3
  62. package/src/server/api/config.js +376 -5
  63. package/src/server/api/convert.js +133 -0
  64. package/src/server/api/dashboard.js +22 -6
  65. package/src/server/api/gemini-channels.js +107 -18
  66. package/src/server/api/gemini-proxy.js +14 -8
  67. package/src/server/api/gemini-sessions.js +1 -1
  68. package/src/server/api/health-check.js +4 -3
  69. package/src/server/api/mcp.js +3 -3
  70. package/src/server/api/opencode-channels.js +497 -0
  71. package/src/server/api/opencode-projects.js +99 -0
  72. package/src/server/api/opencode-proxy.js +207 -0
  73. package/src/server/api/opencode-sessions.js +345 -0
  74. package/src/server/api/opencode-statistics.js +57 -0
  75. package/src/server/api/plugins.js +66 -19
  76. package/src/server/api/prompts.js +2 -2
  77. package/src/server/api/proxy.js +7 -4
  78. package/src/server/api/sessions.js +3 -0
  79. package/src/server/api/settings.js +111 -0
  80. package/src/server/api/skills.js +69 -18
  81. package/src/server/api/workspaces.js +78 -6
  82. package/src/server/codex-proxy-server.js +36 -22
  83. package/src/server/dev-server.js +1 -1
  84. package/src/server/gemini-proxy-server.js +21 -7
  85. package/src/server/index.js +174 -58
  86. package/src/server/opencode-proxy-server.js +5486 -0
  87. package/src/server/proxy-server.js +33 -22
  88. package/src/server/services/agents-service.js +61 -24
  89. package/src/server/services/channel-scheduler.js +9 -5
  90. package/src/server/services/channels.js +64 -37
  91. package/src/server/services/codex-channels.js +56 -43
  92. package/src/server/services/codex-sessions.js +105 -6
  93. package/src/server/services/codex-settings-manager.js +271 -49
  94. package/src/server/services/codex-statistics-service.js +2 -2
  95. package/src/server/services/commands-service.js +84 -25
  96. package/src/server/services/config-export-service.js +7 -45
  97. package/src/server/services/config-registry-service.js +63 -17
  98. package/src/server/services/config-sync-manager.js +160 -7
  99. package/src/server/services/config-templates-service.js +204 -51
  100. package/src/server/services/env-checker.js +50 -13
  101. package/src/server/services/env-manager.js +155 -19
  102. package/src/server/services/favorites.js +5 -3
  103. package/src/server/services/gemini-channels.js +33 -44
  104. package/src/server/services/gemini-statistics-service.js +2 -2
  105. package/src/server/services/mcp-service.js +350 -9
  106. package/src/server/services/model-detector.js +707 -221
  107. package/src/server/services/network-access.js +80 -0
  108. package/src/server/services/opencode-channels.js +208 -0
  109. package/src/server/services/opencode-gateway-converter.js +639 -0
  110. package/src/server/services/opencode-sessions.js +931 -0
  111. package/src/server/services/opencode-settings-manager.js +478 -0
  112. package/src/server/services/opencode-statistics-service.js +255 -0
  113. package/src/server/services/plugins-service.js +479 -22
  114. package/src/server/services/prompts-service.js +53 -11
  115. package/src/server/services/proxy-runtime.js +1 -1
  116. package/src/server/services/repo-scanner-base.js +1 -1
  117. package/src/server/services/response-decoder.js +21 -0
  118. package/src/server/services/security-config.js +1 -1
  119. package/src/server/services/session-cache.js +1 -1
  120. package/src/server/services/skill-service.js +300 -46
  121. package/src/server/services/speed-test.js +464 -186
  122. package/src/server/services/statistics-service.js +2 -2
  123. package/src/server/services/terminal-commands.js +10 -3
  124. package/src/server/services/terminal-config.js +1 -1
  125. package/src/server/services/ui-config.js +1 -1
  126. package/src/server/services/workspace-service.js +57 -100
  127. package/src/server/websocket-server.js +156 -8
  128. package/src/ui/menu.js +49 -40
  129. package/src/utils/port-helper.js +22 -8
  130. package/src/utils/session.js +5 -4
  131. package/dist/web/assets/icons-CO_2OFES.js +0 -1
  132. package/dist/web/assets/index-DI8QOi-E.js +0 -14
  133. package/dist/web/assets/index-uLHGdeZh.css +0 -41
  134. package/dist/web/assets/naive-ui-B1re3c-e.js +0 -1
  135. package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
  136. package/src/server/api/oauth.js +0 -294
  137. package/src/server/api/permissions.js +0 -385
  138. package/src/server/config/oauth-providers.js +0 -68
  139. package/src/server/services/oauth-callback-server.js +0 -284
  140. package/src/server/services/oauth-service.js +0 -378
  141. package/src/server/services/oauth-token-storage.js +0 -135
  142. package/src/server/services/permission-templates-service.js +0 -308
@@ -0,0 +1,478 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { NATIVE_PATHS } = require('../../config/paths');
4
+ const { resolveModelMetadata } = require('../../config/model-metadata');
5
+
6
+ /**
7
+ * 根据模型 ID 查找 limit(context + output)
8
+ * 委托给集中式 model-metadata.js
9
+ */
10
+ function resolveModelLimit(modelId) {
11
+ const meta = resolveModelMetadata(modelId);
12
+ return meta ? meta.limit : null;
13
+ }
14
+
15
+ /**
16
+ * 根据模型 ID 查找定价信息
17
+ * 委托给集中式 model-metadata.js
18
+ */
19
+ function resolveModelCost(modelId) {
20
+ const meta = resolveModelMetadata(modelId);
21
+ return meta ? meta.pricing : null;
22
+ }
23
+
24
+ const CONFIG_DIR = NATIVE_PATHS.opencode.config;
25
+ const CONFIG_PATHS = {
26
+ config: path.join(CONFIG_DIR, 'config.json'),
27
+ opencode: path.join(CONFIG_DIR, 'opencode.json'),
28
+ opencodec: path.join(CONFIG_DIR, 'opencode.jsonc')
29
+ };
30
+ const BACKUP_SUFFIX = '.cc-tool-backup';
31
+ const EMPTY_SENTINEL = '__CC_TOOL_NO_FILE__';
32
+ const PROXY_PROVIDER_ID = 'ctx-proxy';
33
+ const LEGACY_PROVIDER_ID = 'openai';
34
+ const PROXY_API_KEY = 'PROXY_KEY';
35
+ const MANAGED_PROVIDER_MARKER = '__ctx_managed__';
36
+
37
+ function ensureConfigDir() {
38
+ if (!fs.existsSync(CONFIG_DIR)) {
39
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
40
+ }
41
+ }
42
+
43
+ function getBackupPath(filePath) {
44
+ return `${filePath}${BACKUP_SUFFIX}`;
45
+ }
46
+
47
+ function selectConfigPath() {
48
+ if (fs.existsSync(CONFIG_PATHS.opencodec)) return CONFIG_PATHS.opencodec;
49
+ if (fs.existsSync(CONFIG_PATHS.opencode)) return CONFIG_PATHS.opencode;
50
+ if (fs.existsSync(CONFIG_PATHS.config)) return CONFIG_PATHS.config;
51
+ return CONFIG_PATHS.opencode;
52
+ }
53
+
54
+ function stripJsonComments(input) {
55
+ let result = '';
56
+ let inString = false;
57
+ let stringChar = '';
58
+ let i = 0;
59
+
60
+ while (i < input.length) {
61
+ const ch = input[i];
62
+ const next = input[i + 1];
63
+
64
+ if (inString) {
65
+ result += ch;
66
+ if (ch === '\\') {
67
+ if (next) {
68
+ result += next;
69
+ i += 2;
70
+ continue;
71
+ }
72
+ } else if (ch === stringChar) {
73
+ inString = false;
74
+ }
75
+ i += 1;
76
+ continue;
77
+ }
78
+
79
+ if (ch === '"' || ch === '\'') {
80
+ inString = true;
81
+ stringChar = ch;
82
+ result += ch;
83
+ i += 1;
84
+ continue;
85
+ }
86
+
87
+ if (ch === '/' && next === '/') {
88
+ i += 2;
89
+ while (i < input.length && input[i] !== '\n') i += 1;
90
+ continue;
91
+ }
92
+
93
+ if (ch === '/' && next === '*') {
94
+ i += 2;
95
+ while (i < input.length - 1 && !(input[i] === '*' && input[i + 1] === '/')) i += 1;
96
+ i += 2;
97
+ continue;
98
+ }
99
+
100
+ result += ch;
101
+ i += 1;
102
+ }
103
+
104
+ return result;
105
+ }
106
+
107
+ function readConfig(filePath) {
108
+ if (!fs.existsSync(filePath)) return {};
109
+
110
+ const raw = fs.readFileSync(filePath, 'utf8');
111
+ if (!raw.trim()) return {};
112
+
113
+ try {
114
+ if (filePath.endsWith('.jsonc')) {
115
+ return JSON.parse(stripJsonComments(raw));
116
+ }
117
+ return JSON.parse(raw);
118
+ } catch (err) {
119
+ throw new Error(`Failed to parse ${path.basename(filePath)}: ${err.message}`);
120
+ }
121
+ }
122
+
123
+ function writeConfig(filePath, config) {
124
+ ensureConfigDir();
125
+ const content = JSON.stringify(config, null, 2);
126
+ fs.writeFileSync(filePath, content, 'utf8');
127
+ }
128
+
129
+ function normalizeOpenCodeModel(modelId, providerId) {
130
+ const normalized = String(modelId || '').trim();
131
+ if (!normalized) {
132
+ return '';
133
+ }
134
+
135
+ const pid = String(providerId || PROXY_PROVIDER_ID).trim() || PROXY_PROVIDER_ID;
136
+
137
+ // Already has a provider/ prefix - keep as-is only if it matches the expected provider
138
+ if (normalized.includes('/')) {
139
+ return normalized;
140
+ }
141
+ return `${pid}/${normalized}`;
142
+ }
143
+
144
+ function sanitizeProviderKey(name) {
145
+ return String(name || '')
146
+ .toLowerCase()
147
+ .replace(/[^a-z0-9-]/g, '-')
148
+ .replace(/-+/g, '-')
149
+ .replace(/^-|-$/g, '')
150
+ || 'channel';
151
+ }
152
+
153
+ function isLocalProxyBaseUrl(url) {
154
+ const value = String(url || '').trim();
155
+ return value.includes('127.0.0.1') || value.includes('localhost');
156
+ }
157
+
158
+ function isLegacyProxyProvider(provider) {
159
+ if (!provider || typeof provider !== 'object') return false;
160
+ const apiKey = provider?.options?.apiKey;
161
+ const baseUrl = provider?.options?.baseURL;
162
+ return apiKey === PROXY_API_KEY && isLocalProxyBaseUrl(baseUrl);
163
+ }
164
+
165
+ function isManagedProxyProvider(provider) {
166
+ if (!provider || typeof provider !== 'object') return false;
167
+ const apiKey = provider?.options?.apiKey;
168
+ const baseUrl = provider?.options?.baseURL;
169
+ return apiKey === PROXY_API_KEY && isLocalProxyBaseUrl(baseUrl);
170
+ }
171
+
172
+ function isManagedProxyConfig(config) {
173
+ if (!config || typeof config !== 'object') return false;
174
+ // Check legacy single-provider format
175
+ if (isManagedProxyProvider(config?.provider?.[PROXY_PROVIDER_ID])
176
+ || isLegacyProxyProvider(config?.provider?.[LEGACY_PROVIDER_ID])) {
177
+ return true;
178
+ }
179
+ // Check per-channel provider format (any provider with PROXY_API_KEY + local baseURL)
180
+ if (config?.provider && typeof config.provider === 'object') {
181
+ return Object.values(config.provider).some(p => isManagedProxyProvider(p));
182
+ }
183
+ return false;
184
+ }
185
+
186
+ function buildModelsMap(models = [], fallbackModel = '') {
187
+ const map = {};
188
+ const seen = new Set();
189
+
190
+ const add = (value) => {
191
+ if (typeof value !== 'string') return;
192
+ const trimmed = value.trim();
193
+ if (!trimmed) return;
194
+ const key = trimmed.toLowerCase();
195
+ if (seen.has(key)) return;
196
+ seen.add(key);
197
+
198
+ const entry = { name: trimmed };
199
+
200
+ // 注入 limit(context + output),供 OpenCode 显示 "X% used"
201
+ // OpenCode schema 要求 limit 必须同时包含 context 和 output
202
+ const limit = resolveModelLimit(trimmed);
203
+ if (limit) {
204
+ entry.limit = { context: limit.context, output: limit.output };
205
+ }
206
+
207
+ // 注入定价信息,供 OpenCode 计算 cost
208
+ const pricing = resolveModelCost(trimmed);
209
+ if (pricing) {
210
+ entry.cost = {
211
+ input: pricing.input,
212
+ output: pricing.output,
213
+ cache_read: pricing.cacheRead,
214
+ cache_write: pricing.cacheCreation
215
+ };
216
+ }
217
+
218
+ map[trimmed] = entry;
219
+ };
220
+
221
+ if (Array.isArray(models)) {
222
+ models.forEach(add);
223
+ }
224
+ add(fallbackModel);
225
+
226
+ return map;
227
+ }
228
+
229
+ function resolveProxyBaseUrl(config) {
230
+ if (config?.provider?.[PROXY_PROVIDER_ID]?.options?.baseURL) {
231
+ return config.provider[PROXY_PROVIDER_ID].options.baseURL;
232
+ }
233
+ if (config?.provider?.[LEGACY_PROVIDER_ID]?.options?.baseURL) {
234
+ return config.provider[LEGACY_PROVIDER_ID].options.baseURL;
235
+ }
236
+ // Check per-channel managed providers
237
+ if (config?.provider && typeof config.provider === 'object') {
238
+ for (const p of Object.values(config.provider)) {
239
+ if (isManagedProxyProvider(p) && p?.options?.baseURL) {
240
+ return p.options.baseURL;
241
+ }
242
+ }
243
+ }
244
+ return '';
245
+ }
246
+
247
+ function backupConfig(filePath) {
248
+ ensureConfigDir();
249
+ const backupPath = getBackupPath(filePath);
250
+
251
+ if (fs.existsSync(backupPath)) {
252
+ // 防止历史残留备份误伤:若当前配置已回到“非代理托管态”,刷新备份为当前真实配置。
253
+ // 这样 stop/restore 不会把用户配置恢复成陈旧快照(或空文件哨兵)。
254
+ try {
255
+ const backupContent = fs.readFileSync(backupPath, 'utf8');
256
+ if (backupContent === EMPTY_SENTINEL && fs.existsSync(filePath)) {
257
+ const content = fs.readFileSync(filePath, 'utf8');
258
+ fs.writeFileSync(backupPath, content, 'utf8');
259
+ return { success: true, alreadyExists: true };
260
+ }
261
+
262
+ const current = readConfig(filePath);
263
+ if (!isManagedProxyConfig(current)) {
264
+ if (fs.existsSync(filePath)) {
265
+ const content = fs.readFileSync(filePath, 'utf8');
266
+ fs.writeFileSync(backupPath, content, 'utf8');
267
+ } else {
268
+ fs.writeFileSync(backupPath, EMPTY_SENTINEL, 'utf8');
269
+ }
270
+ }
271
+ } catch (error) {
272
+ // ignore backup refresh errors, fallback to existing backup
273
+ }
274
+ return { success: true, alreadyExists: true };
275
+ }
276
+
277
+ if (fs.existsSync(filePath)) {
278
+ const content = fs.readFileSync(filePath, 'utf8');
279
+ fs.writeFileSync(backupPath, content, 'utf8');
280
+ } else {
281
+ fs.writeFileSync(backupPath, EMPTY_SENTINEL, 'utf8');
282
+ }
283
+
284
+ return { success: true, alreadyExists: false };
285
+ }
286
+
287
+ function restoreConfig(filePath) {
288
+ const backupPath = getBackupPath(filePath);
289
+ if (!fs.existsSync(backupPath)) return false;
290
+
291
+ const content = fs.readFileSync(backupPath, 'utf8');
292
+ if (content === EMPTY_SENTINEL) {
293
+ if (fs.existsSync(filePath)) {
294
+ fs.unlinkSync(filePath);
295
+ }
296
+ } else {
297
+ ensureConfigDir();
298
+ fs.writeFileSync(filePath, content, 'utf8');
299
+ }
300
+
301
+ fs.unlinkSync(backupPath);
302
+ return true;
303
+ }
304
+
305
+ function configExists() {
306
+ return fs.existsSync(CONFIG_PATHS.opencodec)
307
+ || fs.existsSync(CONFIG_PATHS.opencode)
308
+ || fs.existsSync(CONFIG_PATHS.config);
309
+ }
310
+
311
+ function hasBackup() {
312
+ return fs.existsSync(getBackupPath(CONFIG_PATHS.opencodec))
313
+ || fs.existsSync(getBackupPath(CONFIG_PATHS.opencode))
314
+ || fs.existsSync(getBackupPath(CONFIG_PATHS.config));
315
+ }
316
+
317
+ function setProxyConfig(proxyPort, options = {}) {
318
+ const filePath = selectConfigPath();
319
+ backupConfig(filePath);
320
+
321
+ const config = readConfig(filePath);
322
+ const next = (config && typeof config === 'object') ? config : {};
323
+
324
+ if (!next.provider || typeof next.provider !== 'object') {
325
+ next.provider = {};
326
+ }
327
+ // 清理历史 openai 代理注入,避免 /models 出现与代理无关的 openai 模型列表。
328
+ if (isLegacyProxyProvider(next.provider[LEGACY_PROVIDER_ID])) {
329
+ delete next.provider[LEGACY_PROVIDER_ID];
330
+ }
331
+ if (Object.prototype.hasOwnProperty.call(next.provider[LEGACY_PROVIDER_ID] || {}, 'model')) {
332
+ delete next.provider[LEGACY_PROVIDER_ID].model;
333
+ }
334
+
335
+ // Remove old single ctx-proxy provider (superseded by per-channel providers)
336
+ delete next.provider[PROXY_PROVIDER_ID];
337
+
338
+ // Remove any previously managed per-channel providers that are no longer in the current list
339
+ Object.keys(next.provider).forEach((key) => {
340
+ if (isManagedProxyProvider(next.provider[key])) {
341
+ delete next.provider[key];
342
+ }
343
+ });
344
+
345
+ const channels = Array.isArray(options.channels) ? options.channels : null;
346
+
347
+ if (channels && channels.length > 0) {
348
+ // Per-channel mode: write one provider entry per channel
349
+ const usedKeys = new Set();
350
+ let firstProviderId = null;
351
+ let firstModelId = null;
352
+
353
+ channels.forEach((ch) => {
354
+ const rawKey = sanitizeProviderKey(ch.providerKey || ch.name || '');
355
+ // Ensure uniqueness
356
+ let key = rawKey;
357
+ let suffix = 2;
358
+ while (usedKeys.has(key)) {
359
+ key = `${rawKey}-${suffix}`;
360
+ suffix += 1;
361
+ }
362
+ usedKeys.add(key);
363
+
364
+ const modelsMap = buildModelsMap(ch.models, ch.model);
365
+ const modelIds = Object.keys(modelsMap);
366
+
367
+ if (modelIds.length > 0) {
368
+ next.provider[key] = {
369
+ npm: '@ai-sdk/openai-compatible',
370
+ name: ch.name || key,
371
+ options: {
372
+ baseURL: `http://127.0.0.1:${proxyPort}/v1`,
373
+ apiKey: PROXY_API_KEY
374
+ },
375
+ models: modelsMap
376
+ };
377
+
378
+ if (firstProviderId === null) {
379
+ firstProviderId = key;
380
+ firstModelId = ch.model || modelIds[0] || null;
381
+ }
382
+ }
383
+ });
384
+
385
+ // Write top-level model pointing to first channel's first model
386
+ const topModel = options.model || (firstProviderId && firstModelId
387
+ ? `${firstProviderId}/${firstModelId}`
388
+ : null);
389
+ if (topModel) {
390
+ const resolved = normalizeOpenCodeModel(topModel, firstProviderId || PROXY_PROVIDER_ID);
391
+ if (resolved) {
392
+ next.model = resolved;
393
+ }
394
+ } else if (isOldManagedModelRef(next.model)) {
395
+ delete next.model;
396
+ }
397
+ } else {
398
+ // Fallback: legacy flat-model mode (single ctx-proxy provider)
399
+ const modelsMap = buildModelsMap(options.models, options.model);
400
+ const modelIds = Object.keys(modelsMap);
401
+
402
+ if (modelIds.length > 0) {
403
+ next.provider[PROXY_PROVIDER_ID] = {
404
+ npm: '@ai-sdk/openai-compatible',
405
+ name: 'CTX Proxy',
406
+ options: {
407
+ baseURL: `http://127.0.0.1:${proxyPort}/v1`,
408
+ apiKey: PROXY_API_KEY
409
+ },
410
+ models: modelsMap
411
+ };
412
+ }
413
+
414
+ const fallbackModel = options.model || modelIds[0] || '';
415
+ if (fallbackModel) {
416
+ const resolvedModel = normalizeOpenCodeModel(fallbackModel, PROXY_PROVIDER_ID);
417
+ if (resolvedModel) {
418
+ next.model = resolvedModel;
419
+ }
420
+ } else if (isOldManagedModelRef(next.model)) {
421
+ delete next.model;
422
+ }
423
+ }
424
+
425
+ writeConfig(filePath, next);
426
+
427
+ return { success: true, port: proxyPort, path: filePath };
428
+ }
429
+
430
+ function isOldManagedModelRef(modelRef) {
431
+ const s = String(modelRef || '');
432
+ return s.startsWith(`${PROXY_PROVIDER_ID}/`) || s.startsWith(`${LEGACY_PROVIDER_ID}/`);
433
+ }
434
+
435
+ function restoreSettings() {
436
+ const restored = [
437
+ restoreConfig(CONFIG_PATHS.opencodec),
438
+ restoreConfig(CONFIG_PATHS.opencode),
439
+ restoreConfig(CONFIG_PATHS.config)
440
+ ].some(Boolean);
441
+
442
+ return { success: restored };
443
+ }
444
+
445
+ function isProxyConfig() {
446
+ try {
447
+ const filePath = selectConfigPath();
448
+ if (!fs.existsSync(filePath)) return false;
449
+ const config = readConfig(filePath);
450
+ const baseUrl = resolveProxyBaseUrl(config);
451
+ return isLocalProxyBaseUrl(baseUrl);
452
+ } catch (err) {
453
+ return false;
454
+ }
455
+ }
456
+
457
+ function getCurrentProxyPort() {
458
+ try {
459
+ if (!isProxyConfig()) return null;
460
+ const filePath = selectConfigPath();
461
+ const config = readConfig(filePath);
462
+ const baseUrl = resolveProxyBaseUrl(config);
463
+ const match = baseUrl.match(/:(\d+)/);
464
+ return match ? parseInt(match[1], 10) : null;
465
+ } catch (err) {
466
+ return null;
467
+ }
468
+ }
469
+
470
+ module.exports = {
471
+ configExists,
472
+ hasBackup,
473
+ setProxyConfig,
474
+ restoreSettings,
475
+ isProxyConfig,
476
+ getCurrentProxyPort,
477
+ CONFIG_PATHS
478
+ };