odant-work 1.0.14 → 1.0.16

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 (81) hide show
  1. package/#system/package-manifest.json +54 -46
  2. package/$server/$folder/handlers/methods/create/$handler/class.js +83 -2
  3. package/$server/$folder/lib/icon/icon.js +14 -0
  4. package/$server/$folder/lib/node/node.js +51 -1
  5. package/$server/$folder/lib/tree/tree.js +5 -1
  6. package/.secretsignore +4 -0
  7. package/MODELS/$ai/$folder/$class/$ai/class.js +57 -2
  8. package/MODELS/$ai/$folder/$class/$provider/class.js +1 -1
  9. package/MODELS/$ai/$folder/$class/$provider/handlers/pages/form/provider/$handler/class.js +13 -0
  10. package/MODELS/$ai/$folder/$class/$provider/handlers/pages/form/provider/$handler/provider.js +479 -0
  11. package/MODELS/$ai/$folder/$class/$provider/handlers/pages/form/trainer/$handler/trainer.js +3 -3
  12. package/MODELS/GigaChat/$provider/$folder/$class/$ai/class.js +10 -20
  13. package/MODELS/GigaChat/$provider/class.js +10 -7
  14. package/MODELS/GigaChat/GigaChat 2 Lite/$ai/class.js +15 -0
  15. package/MODELS/GigaChat/GigaChat 2 Max/$ai/class.js +16 -0
  16. package/MODELS/GigaChat/GigaChat 2 Pro/$ai/class.js +15 -0
  17. package/MODELS/GigaChat/GigaChat Embeddings/$ai/class.js +13 -0
  18. package/MODELS/Local/$provider/$folder/$class/$ai/class.js +3 -3
  19. package/MODELS/Local/$provider/class.js +7 -3
  20. package/MODELS/MTS-AI/$provider/$folder/$class/$ai/class.js +8 -0
  21. package/MODELS/MTS-AI/$provider/class.js +8 -0
  22. package/MODELS/MTS-AI/Cotype Lite/$ai/class.js +15 -0
  23. package/MODELS/MTS-AI/Cotype Pro 2/$ai/class.js +15 -0
  24. package/MODELS/MTS-AI/Cotype Vision/$ai/class.js +14 -0
  25. package/MODELS/T-Bank/$provider/$folder/$class/$ai/class.js +8 -0
  26. package/MODELS/T-Bank/$provider/class.js +8 -0
  27. package/MODELS/T-Bank/T-Lite 2.0/$ai/class.js +15 -0
  28. package/MODELS/T-Bank/T-Pro 2.0/$ai/class.js +16 -0
  29. package/MODELS/T-Bank/T-VoiceKit ASR/$ai/class.js +13 -0
  30. package/MODELS/T-Bank/T-VoiceKit TTS/$ai/class.js +13 -0
  31. package/MODELS/Yandex/$provider/$folder/$class/$ai/class.js +9 -0
  32. package/MODELS/Yandex/$provider/class.js +9 -0
  33. package/MODELS/Yandex/Alice AI (YandexGPT 5 Pro)/$ai/class.js +15 -0
  34. package/MODELS/Yandex/Yandex Embeddings/$ai/class.js +13 -0
  35. package/MODELS/Yandex/Yandex SpeechKit ASR/$ai/class.js +13 -0
  36. package/MODELS/Yandex/Yandex SpeechKit TTS/$ai/class.js +13 -0
  37. package/MODELS/Yandex/YandexART/$ai/class.js +13 -0
  38. package/MODELS/Yandex/YandexGPT 5 Lite/$ai/class.js +15 -0
  39. package/MODELS/Yandex/YandexGPT Reasoning/$ai/class.js +22 -0
  40. package/oda/tools/icons/lib/svg/ai.svg +20 -0
  41. package/package.json +1 -1
  42. package/scripts/check-ignored-code.mjs +3 -0
  43. package/scripts/check-secrets.mjs +1 -1
  44. package/sources/data/progress.md +17 -0
  45. package/sources/host/auth-methods.js +32 -5
  46. package/sources/host/mail.js +93 -14
  47. package/sources/host/watch.js +13 -2
  48. package/sources/modules/agent/index.js +23 -2
  49. package/sources/modules/trainer/halogen.js +128 -29
  50. package/sources/modules/trainer/huggingface.js +11 -5
  51. package/sources/modules/trainer/index.js +135 -2
  52. package/sources/modules/trainer/providers.js +1213 -0
  53. package/sources/modules/user-profile/user-profile.js +7 -1
  54. package/sources/server/class.js +55 -3
  55. package/MODELS/GigaChat/GigaChat Light/$ai/class.js +0 -15
  56. package/MODELS/GigaChat/GigaChat Pro/$ai/class.js +0 -15
  57. package/MODELS/Halogen/$provider/$folder/$class/$ai/class.js +0 -11
  58. package/MODELS/Halogen/$provider/class.js +0 -16
  59. package/MODELS/Halogen/$provider/readme.md +0 -28
  60. package/MODELS/Halogen/Decider-NPU/$ai/class.js +0 -22
  61. package/MODELS/Halogen/Flux2-Klein-NPU/$ai/class.js +0 -21
  62. package/MODELS/Halogen/Qwen3-ASR-NPU/$ai/class.js +0 -21
  63. package/MODELS/Halogen/Qwen3-Embedding-NPU/$ai/class.js +0 -22
  64. package/MODELS/Halogen/Qwen3-Reranker-NPU/$ai/class.js +0 -21
  65. package/MODELS/Halogen/Qwen3-TTS-NPU/$ai/class.js +0 -26
  66. package/MODELS/Halogen/Qwen3.5-2B-NPU/$ai/class.js +0 -23
  67. package/MODELS/Halogen/Qwen3.8-Flash-Next/$ai/class.js +0 -31
  68. package/MODELS/Local/Flux-Image/$ai/class.js +0 -20
  69. package/MODELS/Local/Qwen3-0.6B/$ai/class.js +0 -26
  70. package/MODELS/Local/Qwen3-14B/$ai/class.js +0 -27
  71. package/MODELS/Local/Qwen3-30B-A3B/$ai/class.js +0 -30
  72. package/MODELS/Local/Qwen3-Embedding/$ai/class.js +0 -21
  73. package/MODELS/Local/Qwen3-Voice/$ai/class.js +0 -26
  74. package/MODELS/odant/$provider/$folder/$class/$ai/class.js +0 -11
  75. package/MODELS/odant/$provider/class.js +0 -75
  76. package/MODELS/odant/$provider/readme.md +0 -21
  77. package/MODELS/odant/Qwen3-ASR/$ai/class.js +0 -15
  78. package/MODELS/odant/Qwen3-TTS/$ai/class.js +0 -22
  79. package/MODELS/odant/Qwen3.8 27b/$ai/class.js +0 -11
  80. package/MODELS/odant/Z-Image-Turbo/$ai/class.js +0 -8
  81. package/MODELS/odant/Z-Image-Turbo/$ai/readme.md +0 -24
@@ -0,0 +1,1213 @@
1
+ /**
2
+ * Каталог облачных и локальных провайдеров ИИ (`$provider`) и их типовых моделей (`$ai`).
3
+ *
4
+ * Используется:
5
+ * - для предустановленных российских провайдеров в `/MODELS` (`GigaChat`, `Yandex`, `T-Bank`, `MTS-AI`);
6
+ * - в диалоге создания нового провайдера (`+` на `/MODELS`);
7
+ * - в представлении управления провайдером (`form/provider`): настройка ключей `#secret`, проверка связи
8
+ * и быстрое добавление моделей в 1 клик.
9
+ */
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+
13
+ export const AI_PROVIDER_PRESETS = Object.freeze([
14
+ {
15
+ id: 'GigaChat',
16
+ label: 'GigaChat (Сбер)',
17
+ category: 'ru',
18
+ icon: 'ai:gigachat',
19
+ protocol: 'gigachat',
20
+ baseUrl: 'https://gigachat.devices.sberbank.ru/api/v1/chat/completions',
21
+ authUrl: 'https://ngw.devices.sberbank.ru:9443/api/v2/oauth',
22
+ scope: 'GIGACHAT_API_PERS',
23
+ secretFile: 'gigachat.json',
24
+ secretField: 'token',
25
+ envVar: 'GIGACHAT_KEY',
26
+ keyLabel: 'Ключ авторизации (Authorization Key Base64)',
27
+ keyPlaceholder: 'MDE5... (ключ из личного кабинета developers.sber.ru)',
28
+ description: 'Нейросетевая платформа Сбера: GigaChat 2 Lite, Pro, Max и векторные эмбеддинги.',
29
+ models: [
30
+ {
31
+ id: 'GigaChat 2 Lite',
32
+ label: 'GigaChat 2 Lite',
33
+ model: 'GigaChat-2',
34
+ icon: 'ai:gigachat',
35
+ modality: 'llm',
36
+ maxTokens: 131072,
37
+ maxOutput: 8192,
38
+ functionCalling: true,
39
+ capabilities: ['chat', 'stream', 'tools', 'functions'],
40
+ },
41
+ {
42
+ id: 'GigaChat 2 Pro',
43
+ label: 'GigaChat 2 Pro',
44
+ model: 'GigaChat-2-Pro',
45
+ icon: 'ai:gigachat',
46
+ modality: 'llm',
47
+ maxTokens: 131072,
48
+ maxOutput: 8192,
49
+ functionCalling: true,
50
+ capabilities: ['chat', 'stream', 'tools', 'functions'],
51
+ },
52
+ {
53
+ id: 'GigaChat 2 Max',
54
+ label: 'GigaChat 2 Max',
55
+ model: 'GigaChat-2-Max',
56
+ icon: 'ai:gigachat',
57
+ modality: 'vlm',
58
+ maxTokens: 131072,
59
+ maxOutput: 8192,
60
+ functionCalling: true,
61
+ capabilities: ['chat', 'stream', 'tools', 'functions', 'vision'],
62
+ },
63
+ {
64
+ id: 'GigaChat Embeddings',
65
+ label: 'GigaChat Embeddings',
66
+ model: 'EmbeddingsGigaR',
67
+ icon: 'carbon:search-locate',
68
+ modality: 'embed',
69
+ maxTokens: 4096,
70
+ maxOutput: 1024,
71
+ functionCalling: false,
72
+ capabilities: ['embeddings', 'rag'],
73
+ },
74
+ ],
75
+ },
76
+ {
77
+ id: 'Yandex',
78
+ label: 'Yandex AI (Алиса)',
79
+ category: 'ru',
80
+ icon: 'ai:yandex',
81
+ protocol: 'yandex',
82
+ baseUrl: 'https://llm.api.cloud.yandex.net/v1/chat/completions',
83
+ secretFile: 'yandex.json',
84
+ secretField: 'apiKey',
85
+ envVar: 'YANDEX_API_KEY',
86
+ folderEnvVar: 'YANDEX_FOLDER_ID',
87
+ needsFolderId: true,
88
+ keyLabel: 'API-ключ или IAM-токен Yandex Cloud',
89
+ keyPlaceholder: 'AQVN... (API-ключ сервисного аккаунта Yandex Cloud)',
90
+ folderPlaceholder: 'b1g... (ID каталога Yandex Cloud)',
91
+ description: 'Модели Яндекса (Yandex Cloud Foundation Models): Alice AI / YandexGPT 5 Pro, Lite, Reasoning, YandexART и SpeechKit.',
92
+ models: [
93
+ {
94
+ id: 'Alice AI (YandexGPT 5 Pro)',
95
+ label: 'Alice AI (YandexGPT 5 Pro)',
96
+ model: 'yandexgpt/latest',
97
+ icon: 'ai:yandex',
98
+ modality: 'llm',
99
+ maxTokens: 32768,
100
+ maxOutput: 8192,
101
+ functionCalling: true,
102
+ capabilities: ['chat', 'stream', 'tools', 'functions'],
103
+ },
104
+ {
105
+ id: 'YandexGPT 5 Lite',
106
+ label: 'YandexGPT 5 Lite',
107
+ model: 'yandexgpt-lite/latest',
108
+ icon: 'ai:yandex',
109
+ modality: 'llm',
110
+ maxTokens: 32768,
111
+ maxOutput: 8192,
112
+ functionCalling: true,
113
+ capabilities: ['chat', 'stream', 'tools', 'functions'],
114
+ },
115
+ {
116
+ id: 'YandexGPT Reasoning',
117
+ label: 'YandexGPT Reasoning',
118
+ model: 'yandexgpt/rc',
119
+ icon: 'ai:yandex',
120
+ modality: 'llm',
121
+ maxTokens: 32768,
122
+ maxOutput: 8192,
123
+ functionCalling: true,
124
+ capabilities: ['chat', 'stream', 'tools', 'functions', 'effort', 'thinking'],
125
+ efforts: ['low', 'medium', 'high'],
126
+ },
127
+ {
128
+ id: 'YandexART',
129
+ label: 'YandexART',
130
+ model: 'yandex-art/latest',
131
+ icon: 'ai:stablediffusion',
132
+ modality: 'image',
133
+ maxTokens: 4096,
134
+ maxOutput: 1024,
135
+ functionCalling: false,
136
+ capabilities: ['image', 'text-to-image'],
137
+ },
138
+ {
139
+ id: 'Yandex SpeechKit ASR',
140
+ label: 'Yandex SpeechKit ASR',
141
+ model: 'general:rc',
142
+ icon: 'carbon:microphone',
143
+ modality: 'audio',
144
+ maxTokens: 4096,
145
+ maxOutput: 2048,
146
+ functionCalling: false,
147
+ capabilities: ['stt', 'asr'],
148
+ },
149
+ {
150
+ id: 'Yandex SpeechKit TTS',
151
+ label: 'Yandex SpeechKit TTS',
152
+ model: 'alena',
153
+ icon: 'carbon:volume-up',
154
+ modality: 'audio',
155
+ maxTokens: 4096,
156
+ maxOutput: 2048,
157
+ functionCalling: false,
158
+ capabilities: ['tts', 'voice'],
159
+ },
160
+ {
161
+ id: 'Yandex Embeddings',
162
+ label: 'Yandex Embeddings',
163
+ model: 'text-search-doc/latest',
164
+ icon: 'carbon:search-locate',
165
+ modality: 'embed',
166
+ maxTokens: 4096,
167
+ maxOutput: 1024,
168
+ functionCalling: false,
169
+ capabilities: ['embeddings', 'rag'],
170
+ },
171
+ ],
172
+ },
173
+ {
174
+ id: 'T-Bank',
175
+ label: 'T-Bank AI (T-Pro)',
176
+ category: 'ru',
177
+ icon: 'ai:tbank',
178
+ protocol: 'openai',
179
+ baseUrl: 'https://api.tbank.ru/v1/chat/completions',
180
+ secretFile: 'tbank.json',
181
+ secretField: 'apiKey',
182
+ envVar: 'TBANK_API_KEY',
183
+ keyLabel: 'API-ключ T-Bank AI / T-Pro',
184
+ keyPlaceholder: 'tb-... (ключ доступа к API T-Bank)',
185
+ description: 'Российские языковые и голосовые модели Т-Банка: T-Pro 2.0, T-Lite 2.0 и T-VoiceKit.',
186
+ models: [
187
+ {
188
+ id: 'T-Pro 2.0',
189
+ label: 'T-Pro 2.0',
190
+ model: 't-tech/T-pro-it-2.0',
191
+ icon: 'ai:tbank',
192
+ modality: 'llm',
193
+ maxTokens: 131072,
194
+ maxOutput: 8192,
195
+ functionCalling: true,
196
+ capabilities: ['chat', 'stream', 'tools', 'functions', 'thinking'],
197
+ },
198
+ {
199
+ id: 'T-Lite 2.0',
200
+ label: 'T-Lite 2.0',
201
+ model: 't-tech/T-lite-it-2.0',
202
+ icon: 'ai:tbank',
203
+ modality: 'llm',
204
+ maxTokens: 131072,
205
+ maxOutput: 8192,
206
+ functionCalling: true,
207
+ capabilities: ['chat', 'stream', 'tools', 'functions'],
208
+ },
209
+ {
210
+ id: 'T-VoiceKit ASR',
211
+ label: 'T-VoiceKit ASR',
212
+ model: 't-voicekit-asr',
213
+ icon: 'carbon:microphone',
214
+ modality: 'audio',
215
+ maxTokens: 4096,
216
+ maxOutput: 2048,
217
+ functionCalling: false,
218
+ capabilities: ['stt', 'asr'],
219
+ },
220
+ {
221
+ id: 'T-VoiceKit TTS',
222
+ label: 'T-VoiceKit TTS',
223
+ model: 't-voicekit-tts',
224
+ icon: 'carbon:volume-up',
225
+ modality: 'audio',
226
+ maxTokens: 4096,
227
+ maxOutput: 2048,
228
+ functionCalling: false,
229
+ capabilities: ['tts', 'voice'],
230
+ },
231
+ ],
232
+ },
233
+ {
234
+ id: 'MTS-AI',
235
+ label: 'MTS AI (Cotype)',
236
+ category: 'ru',
237
+ icon: 'ai:mts',
238
+ protocol: 'openai',
239
+ baseUrl: 'https://api.mts.ai/v1/chat/completions',
240
+ secretFile: 'mts.json',
241
+ secretField: 'apiKey',
242
+ envVar: 'MTS_API_KEY',
243
+ keyLabel: 'API-ключ MTS AI (Cotype)',
244
+ keyPlaceholder: 'mts-... (ключ доступа к API MTS AI)',
245
+ description: 'Корпоративные модели МТС ИИ: Cotype Pro 2, Cotype Lite и мультимодальная Cotype Vision.',
246
+ models: [
247
+ {
248
+ id: 'Cotype Pro 2',
249
+ label: 'Cotype Pro 2',
250
+ model: 'cotype-pro-2',
251
+ icon: 'ai:mts',
252
+ modality: 'llm',
253
+ maxTokens: 131072,
254
+ maxOutput: 8192,
255
+ functionCalling: true,
256
+ capabilities: ['chat', 'stream', 'tools', 'functions'],
257
+ },
258
+ {
259
+ id: 'Cotype Lite',
260
+ label: 'Cotype Lite',
261
+ model: 'cotype-lite',
262
+ icon: 'ai:mts',
263
+ modality: 'llm',
264
+ maxTokens: 32768,
265
+ maxOutput: 8192,
266
+ functionCalling: true,
267
+ capabilities: ['chat', 'stream', 'tools', 'functions'],
268
+ },
269
+ {
270
+ id: 'Cotype Vision',
271
+ label: 'Cotype Vision',
272
+ model: 'cotype-vl',
273
+ icon: 'ai:mts',
274
+ modality: 'vlm',
275
+ maxTokens: 32768,
276
+ maxOutput: 8192,
277
+ functionCalling: true,
278
+ capabilities: ['chat', 'stream', 'vision'],
279
+ },
280
+ ],
281
+ },
282
+ {
283
+ id: 'OpenAI',
284
+ label: 'OpenAI (ChatGPT)',
285
+ category: 'global',
286
+ icon: 'ai:chatgpt',
287
+ protocol: 'openai',
288
+ baseUrl: 'https://api.openai.com/v1/chat/completions',
289
+ secretFile: 'openai.json',
290
+ secretField: 'apiKey',
291
+ envVar: 'OPENAI_API_KEY',
292
+ keyLabel: 'OpenAI API Key',
293
+ keyPlaceholder: 'sk-proj-...',
294
+ description: 'Модели OpenAI: GPT-4.1, GPT-4.1 mini, o3, o4-mini, Whisper, TTS и DALL-E / gpt-image-1.',
295
+ models: [
296
+ {
297
+ id: 'GPT-4.1',
298
+ label: 'GPT-4.1',
299
+ model: 'gpt-4.1',
300
+ icon: 'ai:chatgpt',
301
+ modality: 'vlm',
302
+ maxTokens: 1047576,
303
+ maxOutput: 32768,
304
+ functionCalling: true,
305
+ capabilities: ['chat', 'stream', 'tools', 'functions', 'vision'],
306
+ },
307
+ {
308
+ id: 'GPT-4.1 mini',
309
+ label: 'GPT-4.1 mini',
310
+ model: 'gpt-4.1-mini',
311
+ icon: 'ai:chatgpt',
312
+ modality: 'vlm',
313
+ maxTokens: 1047576,
314
+ maxOutput: 32768,
315
+ functionCalling: true,
316
+ capabilities: ['chat', 'stream', 'tools', 'functions', 'vision'],
317
+ },
318
+ {
319
+ id: 'o3',
320
+ label: 'OpenAI o3',
321
+ model: 'o3',
322
+ icon: 'ai:chatgpt',
323
+ modality: 'vlm',
324
+ maxTokens: 200000,
325
+ maxOutput: 65536,
326
+ functionCalling: true,
327
+ capabilities: ['chat', 'stream', 'tools', 'functions', 'vision', 'effort', 'thinking'],
328
+ efforts: ['low', 'medium', 'high'],
329
+ },
330
+ {
331
+ id: 'o4-mini',
332
+ label: 'OpenAI o4-mini',
333
+ model: 'o4-mini',
334
+ icon: 'ai:chatgpt',
335
+ modality: 'vlm',
336
+ maxTokens: 200000,
337
+ maxOutput: 65536,
338
+ functionCalling: true,
339
+ capabilities: ['chat', 'stream', 'tools', 'functions', 'vision', 'effort', 'thinking'],
340
+ efforts: ['low', 'medium', 'high'],
341
+ },
342
+ ],
343
+ },
344
+ {
345
+ id: 'Anthropic',
346
+ label: 'Anthropic (Claude)',
347
+ category: 'global',
348
+ icon: 'ai:claude',
349
+ protocol: 'anthropic',
350
+ baseUrl: 'https://api.anthropic.com/v1/messages',
351
+ secretFile: 'anthropic.json',
352
+ secretField: 'apiKey',
353
+ envVar: 'ANTHROPIC_API_KEY',
354
+ keyLabel: 'Anthropic API Key',
355
+ keyPlaceholder: 'sk-ant-api03-...',
356
+ description: 'Модели Anthropic Claude: Claude Opus 4.6, Claude Sonnet 4.6 и Claude Haiku 4.5 с кэшированием промптов.',
357
+ models: [
358
+ {
359
+ id: 'Claude Sonnet 4.6',
360
+ label: 'Claude Sonnet 4.6',
361
+ model: 'claude-sonnet-4-6',
362
+ icon: 'ai:claude',
363
+ modality: 'vlm',
364
+ maxTokens: 200000,
365
+ maxOutput: 16384,
366
+ functionCalling: true,
367
+ capabilities: ['chat', 'stream', 'tools', 'functions', 'vision', 'effort', 'thinking'],
368
+ efforts: ['low', 'medium', 'high'],
369
+ },
370
+ {
371
+ id: 'Claude Opus 4.6',
372
+ label: 'Claude Opus 4.6',
373
+ model: 'claude-opus-4-6',
374
+ icon: 'ai:claude',
375
+ modality: 'vlm',
376
+ maxTokens: 200000,
377
+ maxOutput: 32768,
378
+ functionCalling: true,
379
+ capabilities: ['chat', 'stream', 'tools', 'functions', 'vision', 'effort', 'thinking'],
380
+ efforts: ['low', 'medium', 'high', 'max'],
381
+ },
382
+ {
383
+ id: 'Claude Haiku 4.5',
384
+ label: 'Claude Haiku 4.5',
385
+ model: 'claude-haiku-4-5',
386
+ icon: 'ai:claude',
387
+ modality: 'vlm',
388
+ maxTokens: 200000,
389
+ maxOutput: 8192,
390
+ functionCalling: true,
391
+ capabilities: ['chat', 'stream', 'tools', 'functions', 'vision'],
392
+ },
393
+ ],
394
+ },
395
+ {
396
+ id: 'DeepSeek',
397
+ label: 'DeepSeek',
398
+ category: 'global',
399
+ icon: 'ai:deepseek',
400
+ protocol: 'openai',
401
+ baseUrl: 'https://api.deepseek.com/v1/chat/completions',
402
+ secretFile: 'deepseek.json',
403
+ secretField: 'apiKey',
404
+ envVar: 'DEEPSEEK_API_KEY',
405
+ keyLabel: 'DeepSeek API Key',
406
+ keyPlaceholder: 'sk-...',
407
+ description: 'Модели DeepSeek: DeepSeek V3 (Chat) и рассуждающая модель DeepSeek R1 (Reasoner).',
408
+ models: [
409
+ {
410
+ id: 'DeepSeek V3',
411
+ label: 'DeepSeek V3 (Chat)',
412
+ model: 'deepseek-chat',
413
+ icon: 'ai:deepseek',
414
+ modality: 'llm',
415
+ maxTokens: 131072,
416
+ maxOutput: 8192,
417
+ functionCalling: true,
418
+ capabilities: ['chat', 'stream', 'tools', 'functions'],
419
+ },
420
+ {
421
+ id: 'DeepSeek R1',
422
+ label: 'DeepSeek R1 (Reasoner)',
423
+ model: 'deepseek-reasoner',
424
+ icon: 'ai:deepseek',
425
+ modality: 'llm',
426
+ maxTokens: 131072,
427
+ maxOutput: 16384,
428
+ functionCalling: true,
429
+ capabilities: ['chat', 'stream', 'tools', 'functions', 'thinking'],
430
+ },
431
+ ],
432
+ },
433
+ {
434
+ id: 'Gemini',
435
+ label: 'Google Gemini',
436
+ category: 'global',
437
+ icon: 'ai:gemini',
438
+ protocol: 'openai',
439
+ baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai/chat/completions',
440
+ secretFile: 'gemini.json',
441
+ secretField: 'apiKey',
442
+ envVar: 'GEMINI_API_KEY',
443
+ keyLabel: 'Google AI Studio API Key',
444
+ keyPlaceholder: 'AIzaSy...',
445
+ description: 'Мультимодальные модели Google Gemini (2.5 Pro и 2.5 Flash) с контекстом до 1 млн токенов.',
446
+ models: [
447
+ {
448
+ id: 'Gemini 2.5 Pro',
449
+ label: 'Gemini 2.5 Pro',
450
+ model: 'gemini-2.5-pro',
451
+ icon: 'ai:gemini',
452
+ modality: 'vlm',
453
+ maxTokens: 1048576,
454
+ maxOutput: 32768,
455
+ functionCalling: true,
456
+ capabilities: ['chat', 'stream', 'tools', 'functions', 'vision', 'effort', 'thinking'],
457
+ efforts: ['low', 'medium', 'high'],
458
+ },
459
+ {
460
+ id: 'Gemini 2.5 Flash',
461
+ label: 'Gemini 2.5 Flash',
462
+ model: 'gemini-2.5-flash',
463
+ icon: 'ai:gemini',
464
+ modality: 'vlm',
465
+ maxTokens: 1048576,
466
+ maxOutput: 16384,
467
+ functionCalling: true,
468
+ capabilities: ['chat', 'stream', 'tools', 'functions', 'vision', 'effort', 'thinking'],
469
+ efforts: ['low', 'medium', 'high'],
470
+ },
471
+ ],
472
+ },
473
+ {
474
+ id: 'OpenRouter',
475
+ label: 'OpenRouter',
476
+ category: 'global',
477
+ icon: 'ai:openrouter',
478
+ protocol: 'openai',
479
+ baseUrl: 'https://openrouter.ai/api/v1/chat/completions',
480
+ secretFile: 'openrouter.json',
481
+ secretField: 'apiKey',
482
+ envVar: 'OPENROUTER_API_KEY',
483
+ keyLabel: 'OpenRouter API Key',
484
+ keyPlaceholder: 'sk-or-v1-...',
485
+ description: 'Единый шлюз ко всем мировым моделям (Qwen, Claude, GPT, Gemini, DeepSeek, Llama, Mistral).',
486
+ models: [
487
+ {
488
+ id: 'Qwen3 235B A22B',
489
+ label: 'Qwen3 235B A22B',
490
+ model: 'qwen/qwen3-235b-a22b',
491
+ icon: 'ai:qwen',
492
+ modality: 'llm',
493
+ maxTokens: 131072,
494
+ maxOutput: 16384,
495
+ functionCalling: true,
496
+ capabilities: ['chat', 'stream', 'tools', 'functions', 'thinking'],
497
+ },
498
+ {
499
+ id: 'DeepSeek R1 (OpenRouter)',
500
+ label: 'DeepSeek R1 (OpenRouter)',
501
+ model: 'deepseek/deepseek-r1',
502
+ icon: 'ai:deepseek',
503
+ modality: 'llm',
504
+ maxTokens: 131072,
505
+ maxOutput: 16384,
506
+ functionCalling: true,
507
+ capabilities: ['chat', 'stream', 'tools', 'functions', 'thinking'],
508
+ },
509
+ ],
510
+ },
511
+ {
512
+ id: 'Grok',
513
+ label: 'xAI (Grok)',
514
+ category: 'global',
515
+ icon: 'ai:grok',
516
+ protocol: 'openai',
517
+ baseUrl: 'https://api.x.ai/v1/chat/completions',
518
+ secretFile: 'grok.json',
519
+ secretField: 'apiKey',
520
+ envVar: 'XAI_API_KEY',
521
+ keyLabel: 'xAI API Key',
522
+ keyPlaceholder: 'xai-...',
523
+ description: 'Модели xAI: Grok 4 и Grok 3 Mini.',
524
+ models: [
525
+ {
526
+ id: 'Grok 4',
527
+ label: 'Grok 4',
528
+ model: 'grok-4',
529
+ icon: 'ai:grok',
530
+ modality: 'vlm',
531
+ maxTokens: 256000,
532
+ maxOutput: 16384,
533
+ functionCalling: true,
534
+ capabilities: ['chat', 'stream', 'tools', 'functions', 'vision', 'thinking'],
535
+ },
536
+ {
537
+ id: 'Grok 3 Mini',
538
+ label: 'Grok 3 Mini',
539
+ model: 'grok-3-mini',
540
+ icon: 'ai:grok',
541
+ modality: 'llm',
542
+ maxTokens: 131072,
543
+ maxOutput: 16384,
544
+ functionCalling: true,
545
+ capabilities: ['chat', 'stream', 'tools', 'functions', 'effort', 'thinking'],
546
+ efforts: ['low', 'high'],
547
+ },
548
+ ],
549
+ },
550
+ {
551
+ id: 'Mistral',
552
+ label: 'Mistral AI',
553
+ category: 'global',
554
+ icon: 'ai:mistral',
555
+ protocol: 'openai',
556
+ baseUrl: 'https://api.mistral.ai/v1/chat/completions',
557
+ secretFile: 'mistral.json',
558
+ secretField: 'apiKey',
559
+ envVar: 'MISTRAL_API_KEY',
560
+ keyLabel: 'Mistral API Key',
561
+ keyPlaceholder: '...',
562
+ description: 'Европейские модели Mistral Large и Codestral.',
563
+ models: [
564
+ {
565
+ id: 'Mistral Large',
566
+ label: 'Mistral Large',
567
+ model: 'mistral-large-latest',
568
+ icon: 'ai:mistral',
569
+ modality: 'llm',
570
+ maxTokens: 131072,
571
+ maxOutput: 8192,
572
+ functionCalling: true,
573
+ capabilities: ['chat', 'stream', 'tools', 'functions'],
574
+ },
575
+ {
576
+ id: 'Codestral',
577
+ label: 'Codestral',
578
+ model: 'codestral-latest',
579
+ icon: 'ai:mistral',
580
+ modality: 'llm',
581
+ maxTokens: 256000,
582
+ maxOutput: 8192,
583
+ functionCalling: true,
584
+ capabilities: ['chat', 'stream', 'tools', 'functions'],
585
+ },
586
+ ],
587
+ },
588
+ {
589
+ id: 'Ollama',
590
+ label: 'Ollama / LM Studio / vLLM',
591
+ category: 'local-api',
592
+ icon: 'ai:ollama',
593
+ protocol: 'openai',
594
+ baseUrl: 'http://127.0.0.1:11434/v1/chat/completions',
595
+ secretFile: 'ollama.json',
596
+ secretField: 'apiKey',
597
+ keyLabel: 'API Key (опционально)',
598
+ keyPlaceholder: 'ollama (не требуется для локального сервера)',
599
+ noKeyRequired: true,
600
+ description: 'Подключение к локальному или сетевому серверу Ollama (:11434), LM Studio (:1234) или vLLM (:8000).',
601
+ models: [],
602
+ },
603
+ {
604
+ id: 'Custom',
605
+ label: 'Свой провайдер (OpenAI API)',
606
+ category: 'custom',
607
+ icon: 'carbon:api',
608
+ protocol: 'openai',
609
+ baseUrl: 'https://api.example.com/v1/chat/completions',
610
+ secretFile: 'custom.json',
611
+ secretField: 'apiKey',
612
+ keyLabel: 'API Key',
613
+ keyPlaceholder: 'sk-...',
614
+ description: 'Любой внешний или корпоративный OpenAI-совместимый сервер ИИ.',
615
+ models: [],
616
+ },
617
+ ]);
618
+
619
+ export function findProviderPreset(idOrName = '') {
620
+ const clean = String(idOrName || '').trim().toLowerCase();
621
+ if (!clean)
622
+ return null;
623
+ return AI_PROVIDER_PRESETS.find(p =>
624
+ p.id.toLowerCase() === clean || p.label.toLowerCase() === clean
625
+ ) || null;
626
+ }
627
+
628
+ /**
629
+ * Сформировать содержимое `MODELS/<Provider>/$provider/class.js` и `MODELS/<Provider>/$provider/$folder/$class/$ai/class.js`.
630
+ */
631
+ export function buildProviderClassScripts(preset, overrides = {}) {
632
+ const label = overrides.label || preset?.label || overrides.id || 'Provider';
633
+ const icon = overrides.icon || preset?.icon || 'carbon:api';
634
+ const protocol = overrides.protocol || preset?.protocol || 'openai';
635
+ const baseUrl = overrides.baseUrl || preset?.baseUrl || 'https://api.openai.com/v1/chat/completions';
636
+ const secretFile = overrides.secretFile || preset?.secretFile || `${String(overrides.id || preset?.id || 'provider').toLowerCase()}.json`;
637
+
638
+ const providerObj = {
639
+ icon,
640
+ label,
641
+ form: 'provider',
642
+ protocol,
643
+ baseUrl,
644
+ };
645
+ if (protocol === 'gigachat') {
646
+ providerObj.authUrl = overrides.authUrl || preset?.authUrl || 'https://ngw.devices.sberbank.ru:9443/api/v2/oauth';
647
+ providerObj.token = `secret:${secretFile}`;
648
+ providerObj.scope = overrides.scope || preset?.scope || 'GIGACHAT_API_PERS';
649
+ }
650
+ else if (protocol === 'yandex') {
651
+ providerObj.apiKey = `secret:${secretFile}`;
652
+ providerObj.folderId = `secret:${secretFile}`;
653
+ }
654
+ else {
655
+ providerObj.apiKey = `secret:${secretFile}`;
656
+ }
657
+
658
+ const aiProtoObj = {
659
+ icon,
660
+ description: `Модель провайдера ${label}`,
661
+ protocol,
662
+ baseUrl,
663
+ functionCalling: true,
664
+ };
665
+ if (protocol === 'gigachat') {
666
+ aiProtoObj.authUrl = providerObj.authUrl;
667
+ aiProtoObj.token = providerObj.token;
668
+ aiProtoObj.scope = providerObj.scope;
669
+ }
670
+ else if (protocol === 'yandex') {
671
+ aiProtoObj.apiKey = providerObj.apiKey;
672
+ aiProtoObj.folderId = providerObj.folderId;
673
+ }
674
+ else {
675
+ aiProtoObj.apiKey = providerObj.apiKey;
676
+ }
677
+
678
+ return {
679
+ providerScript: `export default ${JSON.stringify(providerObj, null, 4)};\n`,
680
+ aiProtoScript: `export default ${JSON.stringify(aiProtoObj, null, 4)};\n`,
681
+ };
682
+ }
683
+
684
+ /**
685
+ * Сформировать `class.js` дочерней модели `$ai`.
686
+ */
687
+ export function buildModelClassScript(modelSpec = {}) {
688
+ const obj = {
689
+ label: modelSpec.label || modelSpec.id || modelSpec.model,
690
+ model: modelSpec.model || modelSpec.id,
691
+ icon: modelSpec.icon || undefined,
692
+ modality: modelSpec.modality || 'llm',
693
+ maxTokens: Number(modelSpec.maxTokens) || 32768,
694
+ maxOutput: Number(modelSpec.maxOutput) || 8192,
695
+ functionCalling: modelSpec.functionCalling !== false,
696
+ capabilities: Array.isArray(modelSpec.capabilities) ? modelSpec.capabilities : ['chat', 'stream'],
697
+ };
698
+ if (Array.isArray(modelSpec.efforts) && modelSpec.efforts.length)
699
+ obj.efforts = modelSpec.efforts;
700
+ if (!obj.icon)
701
+ delete obj.icon;
702
+ return `export default ${JSON.stringify(obj, null, 4)};\n`;
703
+ }
704
+
705
+ /**
706
+ * Развернуть файлы провайдера и его предустановленных моделей на диске (`MODELS/<providerId>`).
707
+ */
708
+ export function scaffoldProviderOnDiskSync({
709
+ rootDir = process.cwd(),
710
+ providerId,
711
+ presetId = '',
712
+ label = '',
713
+ baseUrl = '',
714
+ protocol = '',
715
+ icon = '',
716
+ apiKey = '',
717
+ folderId = '',
718
+ scope = '',
719
+ includeDefaultModels = true,
720
+ } = {}) {
721
+ const preset = findProviderPreset(presetId || providerId);
722
+ const id = String(providerId || preset?.id || 'Provider').trim();
723
+ const providerDir = path.join(rootDir, 'MODELS', id);
724
+ const secretFile = preset?.secretFile || `${id.toLowerCase().replace(/[^a-z0-9_-]/gi, '_')}.json`;
725
+
726
+ const { providerScript, aiProtoScript } = buildProviderClassScripts(preset, {
727
+ id,
728
+ label: label || preset?.label || id,
729
+ baseUrl: baseUrl || preset?.baseUrl,
730
+ protocol: protocol || preset?.protocol,
731
+ icon: icon || preset?.icon,
732
+ scope: scope || preset?.scope,
733
+ secretFile,
734
+ });
735
+
736
+ const provClassFile = path.join(providerDir, '$provider', 'class.js');
737
+ fs.mkdirSync(path.dirname(provClassFile), { recursive: true });
738
+ fs.writeFileSync(provClassFile, providerScript, 'utf-8');
739
+
740
+ const aiProtoFile = path.join(providerDir, '$provider', '$folder', '$class', '$ai', 'class.js');
741
+ fs.mkdirSync(path.dirname(aiProtoFile), { recursive: true });
742
+ fs.writeFileSync(aiProtoFile, aiProtoScript, 'utf-8');
743
+
744
+ const cleanKey = String(apiKey || '').trim();
745
+ const cleanFolder = String(folderId || '').trim();
746
+ if (cleanKey || cleanFolder) {
747
+ const secretDir = path.join(providerDir, '$provider', '#secret');
748
+ fs.mkdirSync(secretDir, { recursive: true });
749
+ const secretPayload = {};
750
+ if ((protocol || preset?.protocol) === 'gigachat') {
751
+ secretPayload.token = cleanKey;
752
+ if (scope || preset?.scope)
753
+ secretPayload.scope = scope || preset.scope;
754
+ }
755
+ else {
756
+ if (cleanKey)
757
+ secretPayload.apiKey = cleanKey;
758
+ if (cleanFolder)
759
+ secretPayload.folderId = cleanFolder;
760
+ }
761
+ fs.writeFileSync(path.join(secretDir, secretFile), JSON.stringify(secretPayload, null, 2) + '\n', { encoding: 'utf-8', mode: 0o600 });
762
+ }
763
+
764
+ const createdModels = [];
765
+ if (includeDefaultModels && Array.isArray(preset?.models)) {
766
+ for (const m of preset.models) {
767
+ const safeModelDir = String(m.id || m.label || m.model).replace(/[<>:"/\\|?*]/g, '_').trim();
768
+ const modelClassFile = path.join(providerDir, safeModelDir, '$ai', 'class.js');
769
+ fs.mkdirSync(path.dirname(modelClassFile), { recursive: true });
770
+ fs.writeFileSync(modelClassFile, buildModelClassScript(m), 'utf-8');
771
+ createdModels.push(`/MODELS/${id}/${safeModelDir}`);
772
+ }
773
+ }
774
+
775
+ return {
776
+ providerPath: `/MODELS/${id}`,
777
+ createdModels,
778
+ };
779
+ }
780
+
781
+ function hasWeightFilesInDirSync(dir) {
782
+ if (!dir || !fs.existsSync(dir))
783
+ return false;
784
+ try {
785
+ return fs.readdirSync(dir).some(f =>
786
+ !f.endsWith('.part')
787
+ && f !== 'hf-manifest.json'
788
+ && /\.(safetensors|gguf|bin|onnx|pt|pth|hgn|hnpw|json)$/i.test(f)
789
+ );
790
+ }
791
+ catch {
792
+ return false;
793
+ }
794
+ }
795
+
796
+ function readSecretJsonSync(rootDir, relFolder, filename) {
797
+ if (!filename)
798
+ return null;
799
+ const candidates = [
800
+ path.join(rootDir, relFolder, '#secret', filename),
801
+ path.join(rootDir, '#system', filename),
802
+ ];
803
+ for (const p of candidates) {
804
+ try {
805
+ if (!fs.existsSync(p))
806
+ continue;
807
+ const raw = fs.readFileSync(p, 'utf-8').trim();
808
+ if (!raw)
809
+ continue;
810
+ if (raw.startsWith('{'))
811
+ return JSON.parse(raw);
812
+ return { value: raw };
813
+ }
814
+ catch { /* ignore */ }
815
+ }
816
+ return null;
817
+ }
818
+
819
+ /**
820
+ * Проверить синхронно, активен ли узел `$provider` или `$ai` в дереве `/MODELS`.
821
+ * Возвращает `{ active: boolean, reason: string, configuredKey: boolean }`.
822
+ */
823
+ export function evaluateAiNodeActivitySync(item, rootDir = process.cwd()) {
824
+ if (!item)
825
+ return { active: false, reason: 'Узел не найден', configuredKey: false };
826
+ const type = item.type;
827
+ if (type !== '$provider' && type !== '$ai')
828
+ return { active: true, reason: '', configuredKey: true };
829
+
830
+ const itemPath = String(item.path || item.short || '');
831
+ const data = item.DATA || {};
832
+
833
+ if (data.enabled === false) {
834
+ return { active: false, reason: 'Отключена в настройках', configuredKey: false };
835
+ }
836
+
837
+ // Проверка отключения родительского провайдера у дочерней модели $ai
838
+ const parts = itemPath.split('/').filter(Boolean); // ['MODELS', '<Provider>', '<Model>']
839
+ const providerId = parts[0] === 'MODELS' ? (parts[1] || '') : '';
840
+ if (type === '$ai' && providerId) {
841
+ try {
842
+ const provClassPath = path.join(rootDir, 'MODELS', providerId, '$provider', 'class.js');
843
+ if (fs.existsSync(provClassPath)) {
844
+ const rawProv = fs.readFileSync(provClassPath, 'utf-8');
845
+ if (/\benabled\s*:\s*false\b/.test(rawProv)) {
846
+ return { active: false, reason: `Провайдер ${providerId} отключён`, configuredKey: false };
847
+ }
848
+ }
849
+ }
850
+ catch { /* ignore */ }
851
+ }
852
+
853
+ // 1. Локальный провайдер /MODELS/Local и его модели
854
+ if (providerId === 'Local' || data.protocol === 'local' || data.protocol === 'halogen') {
855
+ if (type === '$provider') {
856
+ const localDir = path.join(rootDir, 'MODELS', 'Local');
857
+ let installedCount = 0;
858
+ try {
859
+ if (fs.existsSync(localDir)) {
860
+ for (const entry of fs.readdirSync(localDir, { withFileTypes: true })) {
861
+ if (!entry.isDirectory() || entry.name.startsWith('$') || entry.name.startsWith('#') || entry.name === 'models')
862
+ continue;
863
+ if (
864
+ hasWeightFilesInDirSync(path.join(localDir, entry.name, '#weights'))
865
+ || hasWeightFilesInDirSync(path.join(localDir, entry.name, '#adapters'))
866
+ ) {
867
+ installedCount++;
868
+ }
869
+ }
870
+ }
871
+ }
872
+ catch { /* ignore */ }
873
+ const buboManifest1 = path.join(rootDir, 'MODELS', 'Local', 'models', 'qwen38-flash-next-base-ud-q4-k-xl-v1', 'bubo-package.json');
874
+ if (installedCount > 0 || fs.existsSync(buboManifest1)) {
875
+ return { active: true, reason: `Активен (моделей: ${installedCount || 1})`, configuredKey: true };
876
+ }
877
+ return { active: false, reason: 'Нет скачанных локальных моделей (откройте для установки)', configuredKey: false };
878
+ }
879
+
880
+ // Дочерняя модель в /MODELS/Local/<Model>
881
+ const modelName = parts[2] || item.id || '';
882
+ const weightsRel = data.weightsDir || path.join('MODELS', 'Local', modelName, '#weights');
883
+ const adapterRel = data.adapterPath || path.join('MODELS', 'Local', modelName, '#adapters');
884
+ const hasWeights = hasWeightFilesInDirSync(path.resolve(rootDir, weightsRel))
885
+ || hasWeightFilesInDirSync(path.resolve(rootDir, adapterRel))
886
+ || fs.existsSync(path.join(rootDir, 'MODELS', 'Local', 'models', 'qwen38-flash-next-base-ud-q4-k-xl-v1', 'bubo-package.json'));
887
+ if (hasWeights) {
888
+ return { active: true, reason: 'Активна (веса модели на диске)', configuredKey: true };
889
+ }
890
+ return { active: false, reason: 'Веса модели не скачаны в #weights', configuredKey: false };
891
+ }
892
+
893
+ // 2. Внешние и облачные провайдеры ($provider и их $ai)
894
+ const preset = findProviderPreset(providerId);
895
+ const protocol = String(data.protocol || preset?.protocol || 'openai').toLowerCase();
896
+ const baseUrl = String(data.baseUrl || preset?.baseUrl || '').trim();
897
+ const rawKey = String(
898
+ (protocol === 'gigachat' ? (data.token || data.apiKey) : (data.apiKey || data.token))
899
+ || (preset?.secretFile ? `secret:${preset.secretFile}` : '')
900
+ ).trim();
901
+
902
+ // Локальные серверы без ключа (Ollama, LM Studio, vLLM)
903
+ if (preset?.noKeyRequired || protocol === 'ollama' || (!rawKey && /^https?:\/\/(127\.0\.0\.1|localhost)(:\d+)?/i.test(baseUrl))) {
904
+ return { active: true, reason: 'Активна (локальный API)', configuredKey: true };
905
+ }
906
+
907
+ // Проверим env-переменную пресета
908
+ if (preset?.envVar && String(process.env[preset.envVar] || '').trim()) {
909
+ return { active: true, reason: `Активна (ключ из env ${preset.envVar})`, configuredKey: true };
910
+ }
911
+
912
+ if (!rawKey) {
913
+ return { active: false, reason: 'Не настроен API-ключ провайдера', configuredKey: false };
914
+ }
915
+
916
+ if (/^env:/i.test(rawKey)) {
917
+ const envName = rawKey.slice(4).trim();
918
+ const val = String(process.env[envName] || '').trim();
919
+ return val
920
+ ? { active: true, reason: `Активна (env:${envName})`, configuredKey: true }
921
+ : { active: false, reason: `Не задана переменная окружения ${envName}`, configuredKey: false };
922
+ }
923
+
924
+ if (/^secret:/i.test(rawKey)) {
925
+ const secretFile = rawKey.slice(7).trim();
926
+ const modelRel = itemPath.replace(/^\/+/, '');
927
+ const provRel = providerId ? path.join('MODELS', providerId, '$provider') : modelRel;
928
+ const secretObj = readSecretJsonSync(rootDir, path.join(modelRel, item.type || '$ai'), secretFile)
929
+ || readSecretJsonSync(rootDir, provRel, secretFile);
930
+ const keyVal = String(secretObj?.value ?? secretObj?.apiKey ?? secretObj?.token ?? secretObj?.key ?? '').trim();
931
+ if (!keyVal) {
932
+ return { active: false, reason: 'Не настроен API-ключ (откройте провайдер для ввода ключа)', configuredKey: false };
933
+ }
934
+ if (protocol === 'yandex') {
935
+ const folderVal = String(secretObj?.folderId ?? data.folderId ?? process.env.YANDEX_FOLDER_ID ?? '').trim();
936
+ if (!folderVal || /^secret:/i.test(folderVal)) {
937
+ return { active: false, reason: 'Не указан Folder ID каталога Yandex Cloud', configuredKey: false };
938
+ }
939
+ }
940
+ return { active: true, reason: 'Активна (API-ключ настроен)', configuredKey: true };
941
+ }
942
+
943
+ // Прямой литерал ключа
944
+ return { active: true, reason: 'Активна', configuredKey: true };
945
+ }
946
+
947
+ /**
948
+ * Прочитать состояние провайдера `/MODELS/<id>` для формы настройки `form/provider`.
949
+ */
950
+ export function getProviderStatusSync(providerItem, rootDir = process.cwd()) {
951
+ const itemPath = String(providerItem?.path || providerItem?.short || '');
952
+ const parts = itemPath.split('/').filter(Boolean);
953
+ const providerId = parts[1] || providerItem?.id || '';
954
+ const preset = findProviderPreset(providerId);
955
+ const data = providerItem?.DATA || {};
956
+ const protocol = String(data.protocol || preset?.protocol || 'openai');
957
+ const baseUrl = String(data.baseUrl || preset?.baseUrl || '');
958
+ const authUrl = String(data.authUrl || preset?.authUrl || '');
959
+ const scope = String(data.scope || preset?.scope || 'GIGACHAT_API_PERS');
960
+ const enabled = data.enabled !== false;
961
+
962
+ const rawSecretRef = String(
963
+ (protocol === 'gigachat' ? (data.token || data.apiKey) : (data.apiKey || data.token))
964
+ || (preset?.secretFile ? `secret:${preset.secretFile}` : `secret:${providerId.toLowerCase()}.json`)
965
+ ).trim();
966
+ const secretFile = /^secret:/i.test(rawSecretRef)
967
+ ? rawSecretRef.slice(7).trim()
968
+ : (preset?.secretFile || `${providerId.toLowerCase()}.json`);
969
+
970
+ const provRel = path.join('MODELS', providerId, '$provider');
971
+ const secretObj = readSecretJsonSync(rootDir, provRel, secretFile) || {};
972
+ const rawKey = String(secretObj.value ?? secretObj.apiKey ?? secretObj.token ?? secretObj.key ?? '').trim();
973
+ const folderId = String(secretObj.folderId ?? (!/^secret:/i.test(String(data.folderId || '')) ? (data.folderId || '') : '')).trim();
974
+ const maskedKey = rawKey
975
+ ? (rawKey.length > 10 ? `${rawKey.slice(0, 4)}••••••${rawKey.slice(-4)}` : '••••••••')
976
+ : '';
977
+
978
+ const activity = evaluateAiNodeActivitySync(providerItem, rootDir);
979
+
980
+ // Дочерние модели на диске
981
+ const providerDir = path.join(rootDir, 'MODELS', providerId);
982
+ const installedModels = [];
983
+ if (fs.existsSync(providerDir)) {
984
+ try {
985
+ for (const entry of fs.readdirSync(providerDir, { withFileTypes: true })) {
986
+ if (!entry.isDirectory() || entry.name.startsWith('$') || entry.name.startsWith('#'))
987
+ continue;
988
+ const aiClassFile = path.join(providerDir, entry.name, '$ai', 'class.js');
989
+ if (!fs.existsSync(aiClassFile))
990
+ continue;
991
+ let mData = {};
992
+ try {
993
+ const txt = fs.readFileSync(aiClassFile, 'utf-8');
994
+ const jsonPart = txt.replace(/^[^{]*/, '').replace(/;\s*$/, '');
995
+ mData = JSON.parse(jsonPart);
996
+ }
997
+ catch { /* non-json class.js */ }
998
+ const mEnabled = mData.enabled !== false;
999
+ installedModels.push({
1000
+ id: entry.name,
1001
+ path: `/MODELS/${providerId}/${entry.name}`,
1002
+ label: mData.label || entry.name,
1003
+ model: mData.model || entry.name,
1004
+ modality: mData.modality || 'llm',
1005
+ maxTokens: mData.maxTokens || 32768,
1006
+ capabilities: Array.isArray(mData.capabilities) ? mData.capabilities : ['chat', 'stream'],
1007
+ enabled: mEnabled,
1008
+ active: activity.active && mEnabled,
1009
+ });
1010
+ }
1011
+ }
1012
+ catch { /* ignore */ }
1013
+ }
1014
+
1015
+ return {
1016
+ providerId,
1017
+ label: data.label || preset?.label || providerId,
1018
+ icon: data.icon || preset?.icon || 'carbon:api',
1019
+ description: preset?.description || data.description || '',
1020
+ protocol,
1021
+ baseUrl,
1022
+ authUrl,
1023
+ scope,
1024
+ enabled,
1025
+ active: activity.active,
1026
+ statusReason: activity.reason,
1027
+ hasKey: Boolean(rawKey),
1028
+ maskedKey,
1029
+ folderId,
1030
+ needsFolderId: Boolean(preset?.needsFolderId || protocol === 'yandex'),
1031
+ keyLabel: preset?.keyLabel || (protocol === 'gigachat' ? 'Authorization Key (Base64)' : 'API Key'),
1032
+ keyPlaceholder: preset?.keyPlaceholder || 'sk-...',
1033
+ folderPlaceholder: preset?.folderPlaceholder || 'b1g...',
1034
+ secretFile,
1035
+ installedModels,
1036
+ presetModels: preset?.models || [],
1037
+ };
1038
+ }
1039
+
1040
+ /**
1041
+ * Сохранить настройки провайдера (API-ключ / токен / Folder ID в `#secret` + `baseUrl` / `scope` / `enabled` в `class.js`).
1042
+ */
1043
+ export function saveProviderConfigSync({
1044
+ rootDir = process.cwd(),
1045
+ providerId,
1046
+ apiKey,
1047
+ folderId,
1048
+ baseUrl,
1049
+ authUrl,
1050
+ scope,
1051
+ protocol,
1052
+ enabled,
1053
+ } = {}) {
1054
+ const id = String(providerId || '').trim();
1055
+ if (!id)
1056
+ throw new Error('Не указан идентификатор провайдера');
1057
+ const preset = findProviderPreset(id);
1058
+ const provDir = path.join(rootDir, 'MODELS', id, '$provider');
1059
+ const classFile = path.join(provDir, 'class.js');
1060
+ const aiProtoFile = path.join(provDir, '$folder', '$class', '$ai', 'class.js');
1061
+ const secretFile = preset?.secretFile || `${id.toLowerCase().replace(/[^a-z0-9_-]/gi, '_')}.json`;
1062
+ const secretDir = path.join(provDir, '#secret');
1063
+ const secretPath = path.join(secretDir, secretFile);
1064
+
1065
+ let existingSecret = {};
1066
+ try {
1067
+ if (fs.existsSync(secretPath))
1068
+ existingSecret = JSON.parse(fs.readFileSync(secretPath, 'utf-8'));
1069
+ }
1070
+ catch { /* ignore */ }
1071
+
1072
+ const resolvedProto = String(protocol || preset?.protocol || 'openai');
1073
+ if (apiKey !== undefined) {
1074
+ const cleanKey = String(apiKey || '').trim();
1075
+ if (cleanKey) {
1076
+ if (resolvedProto === 'gigachat')
1077
+ existingSecret.token = cleanKey;
1078
+ else
1079
+ existingSecret.apiKey = cleanKey;
1080
+ }
1081
+ else {
1082
+ delete existingSecret.token;
1083
+ delete existingSecret.apiKey;
1084
+ delete existingSecret.value;
1085
+ delete existingSecret.key;
1086
+ }
1087
+ }
1088
+ if (folderId !== undefined) {
1089
+ const cleanFolder = String(folderId || '').trim();
1090
+ if (cleanFolder)
1091
+ existingSecret.folderId = cleanFolder;
1092
+ else
1093
+ delete existingSecret.folderId;
1094
+ }
1095
+ if (scope !== undefined && resolvedProto === 'gigachat') {
1096
+ existingSecret.scope = String(scope || 'GIGACHAT_API_PERS').trim();
1097
+ }
1098
+
1099
+ if (Object.keys(existingSecret).length > 0) {
1100
+ fs.mkdirSync(secretDir, { recursive: true });
1101
+ fs.writeFileSync(secretPath, JSON.stringify(existingSecret, null, 2) + '\n', { encoding: 'utf-8', mode: 0o600 });
1102
+ }
1103
+ else if (fs.existsSync(secretPath)) {
1104
+ fs.rmSync(secretPath, { force: true });
1105
+ }
1106
+
1107
+ // Обновим class.js провайдера и прототипа $ai
1108
+ const { providerScript, aiProtoScript } = buildProviderClassScripts(preset, {
1109
+ id,
1110
+ label: preset?.label || id,
1111
+ icon: preset?.icon,
1112
+ protocol: resolvedProto,
1113
+ baseUrl: baseUrl || preset?.baseUrl,
1114
+ authUrl: authUrl || preset?.authUrl,
1115
+ scope: scope || preset?.scope,
1116
+ secretFile,
1117
+ });
1118
+
1119
+ let finalProviderScript = providerScript;
1120
+ if (enabled === false) {
1121
+ const obj = JSON.parse(providerScript.replace(/^export\s+default\s+/, '').replace(/;\s*$/, ''));
1122
+ obj.enabled = false;
1123
+ finalProviderScript = `export default ${JSON.stringify(obj, null, 4)};\n`;
1124
+ }
1125
+
1126
+ fs.mkdirSync(path.dirname(classFile), { recursive: true });
1127
+ fs.writeFileSync(classFile, finalProviderScript, 'utf-8');
1128
+ fs.mkdirSync(path.dirname(aiProtoFile), { recursive: true });
1129
+ fs.writeFileSync(aiProtoFile, aiProtoScript, 'utf-8');
1130
+
1131
+ return { ok: true, providerId: id, secretFile };
1132
+ }
1133
+
1134
+ /**
1135
+ * Добавить одну или несколько моделей (`$ai`) в провайдер `/MODELS/<providerId>`.
1136
+ */
1137
+ export function addProviderModelsSync({
1138
+ rootDir = process.cwd(),
1139
+ providerId,
1140
+ models = [],
1141
+ } = {}) {
1142
+ const id = String(providerId || '').trim();
1143
+ if (!id)
1144
+ throw new Error('Не указан провайдер');
1145
+ const preset = findProviderPreset(id);
1146
+ const providerDir = path.join(rootDir, 'MODELS', id);
1147
+ const list = Array.isArray(models) ? models : [models];
1148
+ const created = [];
1149
+
1150
+ for (const raw of list) {
1151
+ const spec = typeof raw === 'string'
1152
+ ? (preset?.models?.find(m => m.id === raw || m.model === raw) || { id: raw, label: raw, model: raw })
1153
+ : raw;
1154
+ if (!spec || !(spec.id || spec.model || spec.label))
1155
+ continue;
1156
+ const rawDirName = String(spec.id || spec.label || spec.model).replace(/[<>:"/\\|?*]/g, '_').trim();
1157
+ if (!rawDirName)
1158
+ continue;
1159
+ const modelClassFile = path.join(providerDir, rawDirName, '$ai', 'class.js');
1160
+ fs.mkdirSync(path.dirname(modelClassFile), { recursive: true });
1161
+ fs.writeFileSync(modelClassFile, buildModelClassScript({
1162
+ ...spec,
1163
+ icon: spec.icon || preset?.icon || 'carbon:machine-learning-model',
1164
+ }), 'utf-8');
1165
+ created.push(`/MODELS/${id}/${rawDirName}`);
1166
+ }
1167
+
1168
+ return { ok: true, created };
1169
+ }
1170
+
1171
+ /**
1172
+ * Включить или отключить модель `$ai` или провайдер `$provider` (`enabled: true/false`).
1173
+ */
1174
+ export function toggleAiNodeEnabledSync({
1175
+ rootDir = process.cwd(),
1176
+ nodePath,
1177
+ enabled = true,
1178
+ } = {}) {
1179
+ const clean = String(nodePath || '').replace(/^\/+/, '');
1180
+ const parts = clean.split('/').filter(Boolean); // ['MODELS', '<Provider>', '<Model>?']
1181
+ if (parts[0] !== 'MODELS' || !parts[1])
1182
+ throw new Error('Некорректный путь узла ИИ: ' + nodePath);
1183
+ const isProvider = parts.length === 2;
1184
+ const classFile = isProvider
1185
+ ? path.join(rootDir, 'MODELS', parts[1], '$provider', 'class.js')
1186
+ : path.join(rootDir, 'MODELS', parts[1], parts[2], '$ai', 'class.js');
1187
+
1188
+ if (!fs.existsSync(classFile))
1189
+ throw new Error('Файл class.js не найден: ' + classFile);
1190
+
1191
+ const raw = fs.readFileSync(classFile, 'utf-8');
1192
+ let updated = raw;
1193
+ try {
1194
+ const jsonPart = raw.replace(/^[^{]*/, '').replace(/;\s*$/, '');
1195
+ const obj = JSON.parse(jsonPart);
1196
+ if (enabled)
1197
+ delete obj.enabled;
1198
+ else
1199
+ obj.enabled = false;
1200
+ updated = `export default ${JSON.stringify(obj, null, 4)};\n`;
1201
+ }
1202
+ catch {
1203
+ if (/\benabled\s*:\s*(true|false)\b/.test(raw)) {
1204
+ updated = raw.replace(/\benabled\s*:\s*(true|false)\b/, `enabled: ${Boolean(enabled)}`);
1205
+ }
1206
+ else {
1207
+ updated = raw.replace(/export\s+default\s*\{/, `export default {\n enabled: ${Boolean(enabled)},`);
1208
+ }
1209
+ }
1210
+ fs.writeFileSync(classFile, updated, 'utf-8');
1211
+ return { ok: true, path: '/' + parts.join('/'), enabled: Boolean(enabled) };
1212
+ }
1213
+