@iola_adm/iola-cli 0.1.113 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -131,7 +131,37 @@ CLI использует модель `iola-router:qwen3-1.7b-v4-q8` из GGUF-
131
131
  /model
132
132
  ```
133
133
 
134
- В интерактивном CLI команда `/model` переключает локальную модель IOLA, API-профили OpenAI/OpenRouter и Codex CLI. Для OpenRouter выбор устроен так: сначала выбирается разработчик моделей, затем CLI показывает до 30 самых свежих моделей для текстовой работы с датой релиза и размером контекста. В списке моделей `0` возвращает к выбору разработчика.
134
+ В интерактивном CLI команда `/model` переключает локальные модели, российские AI-провайдеры YandexGPT/GigaChat, API-профили OpenAI/OpenRouter и Codex CLI. Российские провайдеры вызываются напрямую, без gateway/proxy. Для OpenRouter выбор устроен так: сначала выбирается разработчик моделей, затем CLI показывает до 30 самых свежих моделей для текстовой работы с датой релиза и размером контекста. В списке моделей `0` возвращает к выбору разработчика.
135
+
136
+ В локальном выборе доступны:
137
+
138
+ - штатная модель IOLA `iola-router:qwen3-1.7b-v4-q8`;
139
+ - установленные и рекомендуемые модели Ollama;
140
+ - ручной ввод имени любой Ollama-модели, например `qwen3:4b` или `llama3.2:3b`.
141
+
142
+ Если выбранная Ollama-модель еще не скачана, CLI предложит выполнить `ollama pull`.
143
+
144
+ Российские AI:
145
+
146
+ - Yandex AI Studio / YandexGPT: документация `https://yandex.cloud/ru/docs/foundation-models/`, аутентификация `https://yandex.cloud/ru/docs/ai-studio/api-ref/authentication`, тарифы `https://yandex.cloud/ru/docs/foundation-models/pricing`;
147
+ - GigaChat: документация `https://developers.sber.ru/docs/ru/gigachat/overview`, получение токена `https://developers.sber.ru/docs/ru/gigachat/api/reference/rest/post-token`, тарифы `https://developers.sber.ru/docs/ru/gigachat/tariffs`.
148
+
149
+ ```bash
150
+ iola ai key set yandexgpt
151
+ iola ai setup yandexgpt --model yandexgpt-lite/latest
152
+
153
+ iola ai key set gigachat
154
+ iola ai setup gigachat --model GigaChat-2
155
+ ```
156
+
157
+ У GigaChat для физических лиц есть Freemium-лимит на токены; для больших объемов используются платные пакеты. У YandexGPT тарификация идет через Yandex Cloud по токенам и квотам аккаунта, актуальные бесплатные гранты или лимиты нужно проверять в консоли Yandex Cloud.
158
+
159
+ Зарубежные API-ключи:
160
+
161
+ - OpenAI Platform: регистрация `https://platform.openai.com/`, ключи `https://platform.openai.com/api-keys`;
162
+ - OpenRouter: регистрация `https://openrouter.ai/`, ключи `https://openrouter.ai/settings/keys`.
163
+
164
+ Ключи сохраняются локально командой `iola ai key set openai` или `iola ai key set openrouter`. Важно: оплата российскими банковскими картами для OpenAI Platform и OpenRouter может быть невозможна. Перед настройкой платных API проверьте доступный способ оплаты в личном кабинете сервиса.
135
165
 
136
166
  Ollama остается опциональным runtime:
137
167
 
@@ -170,7 +200,7 @@ iola version --check
170
200
  - интеграция с публичным MCP-сервером Йошкар-Олы;
171
201
  - поиск и выгрузка открытых данных;
172
202
  - локальная SQLite-БД, история, сессии и FTS-поиск;
173
- - AI-профили для IOLA local, Ollama, OpenAI, OpenRouter и Codex CLI;
203
+ - AI-профили для IOLA local, Ollama, YandexGPT, GigaChat, OpenAI, OpenRouter и Codex CLI;
174
204
  - локальный tool-agent для модели IOLA с tools `search_data`, `search_entities`, `resolve_entity_field`, `get_card`, `export_report`, `file_read`, `browser_open`;
175
205
  - ленивые skills, toolsets, permissions, memory, hooks и готовые agents;
176
206
  - subagents, skill bundles, layered settings, usage/budget accounting и trajectory export;
package/bin/iola.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.1.113",
3
+ "version": "0.2.1",
4
4
  "description": "CLI и AI-агент городского округа Йошкар-Ола.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/adm-iola/iola-cli#readme",
package/src/cli.js CHANGED
@@ -4,6 +4,7 @@ import { createServer } from "node:http";
4
4
  import { appendFile, copyFile, cp, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
7
+ import { randomUUID } from "node:crypto";
7
8
  import { emitKeypressEvents } from "node:readline";
8
9
  import readline from "node:readline/promises";
9
10
  import { Readable } from "node:stream";
@@ -164,6 +165,20 @@ const DEFAULT_AI_CONFIG = {
164
165
  baseUrl: "https://openrouter.ai/api/v1",
165
166
  networkMode: "gateway",
166
167
  },
168
+ yandexgpt: {
169
+ provider: "yandexgpt",
170
+ model: "yandexgpt-lite/latest",
171
+ baseUrl: "https://llm.api.cloud.yandex.net/foundationModels/v1",
172
+ networkMode: "direct",
173
+ },
174
+ gigachat: {
175
+ provider: "gigachat",
176
+ model: "GigaChat-2",
177
+ baseUrl: "https://gigachat.devices.sberbank.ru/api/v1",
178
+ authUrl: "https://ngw.devices.sberbank.ru:9443/api/v2/oauth",
179
+ scope: "GIGACHAT_API_PERS",
180
+ networkMode: "direct",
181
+ },
167
182
  codex: {
168
183
  provider: "codex",
169
184
  model: "gpt-5.5",
@@ -611,21 +626,25 @@ Usage:
611
626
  iola update
612
627
  iola ask TEXT [--profile NAME] [--model MODEL] [--tools] [--files] [--plan] [--trace] [--reasoning fast|verify|vote] [--output FILE] [--schema json|table] [--events] [--no-history] [--bare] [--quiet] [--no-color] [--fail-on-empty]
613
628
  iola data LAYER [--limit 10] [--search TEXT] [--where FIELD=VALUE] [--columns a,b,c] [--format table|json|csv]
614
- iola ai ask TEXT [--provider iola|ollama|openai|openrouter] [--model MODEL]
629
+ iola ai ask TEXT [--provider iola|ollama|yandexgpt|gigachat|openai|openrouter] [--model MODEL]
615
630
  iola ai context TEXT [--json]
631
+ iola ai key set yandexgpt
632
+ iola ai key set gigachat
616
633
  iola ai key set openai
617
634
  iola ai key set openrouter
618
635
  iola ai key status
619
- iola ai key delete openai|openrouter
636
+ iola ai key delete yandexgpt|gigachat|openai|openrouter
620
637
  iola ai profiles
621
638
  iola ai profile add NAME --provider PROVIDER --model MODEL
622
639
  iola ai profile use NAME
623
640
  iola ai profile delete NAME
624
- iola ai models iola|ollama|openai|openrouter|codex [--search TEXT]
641
+ iola ai models iola|ollama|yandexgpt|gigachat|openai|openrouter|codex [--search TEXT]
625
642
  iola ai doctor [--json]
626
643
  iola ai setup
627
644
  iola ai setup iola [--yes] [--force]
628
645
  iola ai setup ollama [--yes] [--model MODEL]
646
+ iola ai setup yandexgpt [--model MODEL]
647
+ iola ai setup gigachat [--model MODEL]
629
648
  iola health [--json]
630
649
  iola layers [--json]
631
650
  iola schools [--limit 10] [--search TEXT] [--where FIELD=VALUE] [--columns a,b,c] [--format table|json|csv]
@@ -753,9 +772,14 @@ async function getAiReadiness() {
753
772
  const iola = await hasUsableIolaModel();
754
773
  const openai = Boolean(process.env.OPENAI_API_KEY || secrets.openai?.apiKey);
755
774
  const openrouter = Boolean(process.env.OPENROUTER_API_KEY || secrets.openrouter?.apiKey);
775
+ const yandexgpt = Boolean((process.env.YANDEXGPT_API_KEY || process.env.YANDEX_CLOUD_API_KEY || secrets.yandexgpt?.apiKey)
776
+ && (process.env.YANDEXGPT_FOLDER_ID || process.env.YANDEX_CLOUD_FOLDER_ID || secrets.yandexgpt?.folderId));
777
+ const gigachat = Boolean(process.env.GIGACHAT_AUTH_KEY || process.env.GIGACHAT_API_KEY || secrets.gigachat?.apiKey);
756
778
  const providerReady = {
757
779
  iola,
758
780
  ollama,
781
+ yandexgpt,
782
+ gigachat,
759
783
  openai,
760
784
  openrouter,
761
785
  codex,
@@ -765,10 +789,12 @@ async function getAiReadiness() {
765
789
  activeProfile: activeProfileName,
766
790
  activeProvider: activeProfile.provider || "-",
767
791
  activeModel: activeProfile.model || "-",
768
- anyReady: Boolean(iola || ollama || openai || openrouter || codex),
792
+ anyReady: Boolean(iola || ollama || yandexgpt || gigachat || openai || openrouter || codex),
769
793
  profiles: config.ai.profiles || {},
770
794
  iola,
771
795
  ollama,
796
+ yandexgpt,
797
+ gigachat,
772
798
  openai,
773
799
  openrouter,
774
800
  codex,
@@ -776,7 +802,7 @@ async function getAiReadiness() {
776
802
  }
777
803
 
778
804
  function getFallbackAiProfile(readiness) {
779
- const priority = ["iola", "openai", "openrouter", "codex", "ollama"];
805
+ const priority = ["iola", "ollama", "yandexgpt", "gigachat", "openai", "openrouter", "codex"];
780
806
  for (const provider of priority) {
781
807
  if (!readiness[provider]) continue;
782
808
  const entry = Object.entries(readiness.profiles || {}).find(([, profile]) => profile.provider === provider);
@@ -1612,6 +1638,8 @@ function buildAgentStatusLine(state) {
1612
1638
  const kind = {
1613
1639
  iola: "IOLA local",
1614
1640
  ollama: "локальная",
1641
+ yandexgpt: "YandexGPT",
1642
+ gigachat: "GigaChat",
1615
1643
  openai: "API",
1616
1644
  openrouter: "API",
1617
1645
  codex: "Codex",
@@ -2051,6 +2079,8 @@ async function initCli(args = []) {
2051
2079
  console.log("Для настройки AI используйте:");
2052
2080
  console.log(" iola ai setup iola --yes");
2053
2081
  console.log(" iola ai setup ollama");
2082
+ console.log(" iola ai key set yandexgpt");
2083
+ console.log(" iola ai key set gigachat");
2054
2084
  console.log(" iola ai key set openai");
2055
2085
  console.log(" iola ai setup openai --model gpt-4.1-mini");
2056
2086
  return;
@@ -2073,21 +2103,25 @@ async function handleAi(args) {
2073
2103
  if (subcommand === "help") {
2074
2104
  await showBanner();
2075
2105
  console.log(`AI-команды:
2076
- iola ai ask TEXT [--provider iola|ollama|openai|openrouter] [--model MODEL]
2106
+ iola ai ask TEXT [--provider iola|ollama|yandexgpt|gigachat|openai|openrouter] [--model MODEL]
2077
2107
  iola ai context TEXT [--json]
2108
+ iola ai key set yandexgpt
2109
+ iola ai key set gigachat
2078
2110
  iola ai key set openai
2079
2111
  iola ai key set openrouter
2080
2112
  iola ai key status
2081
- iola ai key delete openai|openrouter
2113
+ iola ai key delete yandexgpt|gigachat|openai|openrouter
2082
2114
  iola ai profiles
2083
- iola ai profile add NAME --provider iola|ollama|openai|openrouter|codex --model MODEL
2115
+ iola ai profile add NAME --provider iola|ollama|yandexgpt|gigachat|openai|openrouter|codex --model MODEL
2084
2116
  iola ai profile use NAME
2085
2117
  iola ai profile delete NAME
2086
- iola ai models iola|ollama|openai|openrouter|codex [--search TEXT]
2118
+ iola ai models iola|ollama|yandexgpt|gigachat|openai|openrouter|codex [--search TEXT]
2087
2119
  iola ai doctor [--json]
2088
2120
  iola ai setup
2089
2121
  iola ai setup iola [--yes] [--force]
2090
2122
  iola ai setup ollama [--yes] [--model MODEL]
2123
+ iola ai setup yandexgpt [--model MODEL]
2124
+ iola ai setup gigachat [--model MODEL]
2091
2125
  iola ai setup openai [--model MODEL]
2092
2126
  iola ai setup openrouter [--model MODEL]
2093
2127
 
@@ -4187,9 +4221,14 @@ async function aiSetup(args) {
4187
4221
  return;
4188
4222
  }
4189
4223
 
4190
- if (provider === "openai" || provider === "openrouter") {
4224
+ if (provider === "openai" || provider === "openrouter" || provider === "yandexgpt" || provider === "gigachat") {
4191
4225
  const options = parseOptions(args.slice(1));
4192
- const model = options.model || (provider === "openai" ? "gpt-4.1-mini" : "openai/gpt-4.1-mini");
4226
+ const model = options.model || {
4227
+ openai: "gpt-4.1-mini",
4228
+ openrouter: "openai/gpt-4.1-mini",
4229
+ yandexgpt: "yandexgpt-lite/latest",
4230
+ gigachat: "GigaChat-2",
4231
+ }[provider];
4193
4232
  const profileName = options.name || provider;
4194
4233
  const profile = buildProfileFromOptions(provider, { ...options, model });
4195
4234
  const config = await loadConfig();
@@ -4208,7 +4247,13 @@ async function aiSetup(args) {
4208
4247
  });
4209
4248
  console.log(`AI-профиль ${profileName} сохранен и выбран в ${CONFIG_FILE}`);
4210
4249
  console.log(`Ключ сохраните командой: iola ai key set ${provider}`);
4211
- console.log(`Также можно использовать переменную окружения ${provider === "openai" ? "OPENAI_API_KEY" : "OPENROUTER_API_KEY"}.`);
4250
+ const envHint = {
4251
+ openai: "OPENAI_API_KEY",
4252
+ openrouter: "OPENROUTER_API_KEY",
4253
+ yandexgpt: "YANDEXGPT_API_KEY и YANDEXGPT_FOLDER_ID",
4254
+ gigachat: "GIGACHAT_AUTH_KEY",
4255
+ }[provider];
4256
+ console.log(`Также можно использовать переменную окружения ${envHint}.`);
4212
4257
  return;
4213
4258
  }
4214
4259
 
@@ -4259,10 +4304,12 @@ async function handleAiKey(args) {
4259
4304
  }
4260
4305
 
4261
4306
  throw new Error(`Unknown key command. Use:
4307
+ iola ai key set yandexgpt
4308
+ iola ai key set gigachat
4262
4309
  iola ai key set openai
4263
4310
  iola ai key set openrouter
4264
4311
  iola ai key status
4265
- iola ai key delete openai|openrouter`);
4312
+ iola ai key delete yandexgpt|gigachat|openai|openrouter`);
4266
4313
  }
4267
4314
 
4268
4315
  async function handleAiProfile(args) {
@@ -4304,8 +4351,8 @@ async function aiModels(args) {
4304
4351
  const [provider] = args;
4305
4352
  const options = parseOptions(args.slice(1));
4306
4353
 
4307
- if (!["iola", "ollama", "openai", "openrouter", "codex"].includes(provider)) {
4308
- throw new Error("Провайдер обязателен: iola ai models iola|ollama|openai|openrouter|codex");
4354
+ if (!["iola", "ollama", "yandexgpt", "gigachat", "openai", "openrouter", "codex"].includes(provider)) {
4355
+ throw new Error("Провайдер обязателен: iola ai models iola|ollama|yandexgpt|gigachat|openai|openrouter|codex");
4309
4356
  }
4310
4357
 
4311
4358
  const models = await listAiModels(provider);
@@ -4412,6 +4459,23 @@ async function listAiModels(provider) {
4412
4459
  .sort((left, right) => left.id.localeCompare(right.id));
4413
4460
  }
4414
4461
 
4462
+ if (provider === "yandexgpt") {
4463
+ return [
4464
+ { id: "yandexgpt-lite/latest", provider: "yandexgpt", note: "быстрая и недорогая модель" },
4465
+ { id: "yandexgpt/latest", provider: "yandexgpt", note: "YandexGPT Pro, latest" },
4466
+ { id: "yandexgpt/rc", provider: "yandexgpt", note: "YandexGPT Pro, release candidate" },
4467
+ ];
4468
+ }
4469
+
4470
+ if (provider === "gigachat") {
4471
+ return [
4472
+ { id: "GigaChat-2", provider: "gigachat", note: "основная модель" },
4473
+ { id: "GigaChat-2-Pro", provider: "gigachat", note: "повышенное качество" },
4474
+ { id: "GigaChat-2-Max", provider: "gigachat", note: "максимальное качество" },
4475
+ { id: "GigaChat", provider: "gigachat", note: "legacy/fallback" },
4476
+ ];
4477
+ }
4478
+
4415
4479
  const version = await getCommandVersion("codex", ["--version"]);
4416
4480
  return [
4417
4481
  { id: "gpt-5.5", provider: "codex", note: version },
@@ -4562,7 +4626,7 @@ async function printAiProfiles() {
4562
4626
  baseUrl: profile.baseUrl || "-",
4563
4627
  mode: profile.provider === "codex"
4564
4628
  ? `sandbox=${profile.sandbox || "read-only"}, approval=${profile.approval || "never"}`
4565
- : (profile.provider === "openai" || profile.provider === "openrouter" ? `network=${getAiNetworkMode(profile)}` : "-"),
4629
+ : (profile.provider === "openai" || profile.provider === "openrouter" || profile.provider === "yandexgpt" || profile.provider === "gigachat" ? `network=${getAiNetworkMode(profile)}` : "-"),
4566
4630
  }));
4567
4631
 
4568
4632
  printTable(rows, [
@@ -4595,8 +4659,8 @@ async function addAiProfile(name, args) {
4595
4659
  const options = parseOptions(args);
4596
4660
  const provider = options.provider;
4597
4661
 
4598
- if (!["iola", "ollama", "openai", "openrouter", "codex"].includes(provider)) {
4599
- throw new Error("Провайдер должен быть iola, ollama, openai, openrouter или codex.");
4662
+ if (!["iola", "ollama", "yandexgpt", "gigachat", "openai", "openrouter", "codex"].includes(provider)) {
4663
+ throw new Error("Провайдер должен быть iola, ollama, yandexgpt, gigachat, openai, openrouter или codex.");
4600
4664
  }
4601
4665
 
4602
4666
  const profile = buildProfileFromOptions(provider, options);
@@ -4676,6 +4740,7 @@ async function deleteAiProfile(name) {
4676
4740
 
4677
4741
  function buildProfileFromOptions(provider, options) {
4678
4742
  const defaults = DEFAULT_AI_CONFIG.ai.profiles[provider === "ollama" || provider === "iola" ? "local" : provider];
4743
+ if (!defaults) throw new Error(`Неизвестный AI-провайдер: ${provider}`);
4679
4744
  const profile = {
4680
4745
  ...defaults,
4681
4746
  provider,
@@ -4724,13 +4789,15 @@ async function useAiProvider(args) {
4724
4789
 
4725
4790
  const provider = providerOrProfile;
4726
4791
 
4727
- if (provider !== "iola" && provider !== "ollama" && provider !== "openai" && provider !== "openrouter" && provider !== "codex") {
4728
- throw new Error("Провайдер должен быть iola, ollama, openai, openrouter, codex или именем AI-профиля.");
4792
+ if (!["iola", "ollama", "yandexgpt", "gigachat", "openai", "openrouter", "codex"].includes(provider)) {
4793
+ throw new Error("Провайдер должен быть iola, ollama, yandexgpt, gigachat, openai, openrouter, codex или именем AI-профиля.");
4729
4794
  }
4730
4795
 
4731
4796
  const defaultModel = {
4732
4797
  iola: IOLA_LOCAL_MODEL,
4733
4798
  ollama: config.ai.provider === "ollama" ? config.ai.model : IOLA_LOCAL_OLLAMA_MODEL,
4799
+ yandexgpt: config.ai.provider === "yandexgpt" ? config.ai.model : "yandexgpt-lite/latest",
4800
+ gigachat: config.ai.provider === "gigachat" ? config.ai.model : "GigaChat-2",
4734
4801
  openai: config.ai.provider === "openai" ? config.ai.model : "gpt-4.1-mini",
4735
4802
  openrouter: config.ai.provider === "openrouter" ? config.ai.model : "openai/gpt-4.1-mini",
4736
4803
  codex: config.ai.provider === "codex" ? config.ai.model : "gpt-5.5",
@@ -4775,6 +4842,7 @@ function normalizeModelMenuTarget(value = "") {
4775
4842
  const normalized = String(value || "").trim().toLocaleLowerCase("ru-RU");
4776
4843
  if (!normalized) return "";
4777
4844
  if (["local", "локальная", "локально", "iola", "иола", "ollama"].includes(normalized)) return "local";
4845
+ if (["ru", "rus", "russian", "российские", "российская", "россия", "яндекс", "yandex", "yandexgpt", "gigachat", "гигачат"].includes(normalized)) return normalized === "yandexgpt" || normalized === "яндекс" || normalized === "yandex" ? "yandexgpt" : normalized === "gigachat" || normalized === "гигачат" ? "gigachat" : "russian";
4778
4846
  if (["api", "апи"].includes(normalized)) return "api";
4779
4847
  if (normalized === "openai") return "openai";
4780
4848
  if (normalized === "openrouter" || normalized === "router") return "openrouter";
@@ -4784,19 +4852,20 @@ function normalizeModelMenuTarget(value = "") {
4784
4852
 
4785
4853
  async function chooseModelTarget() {
4786
4854
  console.log("Выберите AI-подключение:");
4787
- console.log(" 1. Локальная модель IOLA");
4788
- console.log(" 2. API (OpenAI/OpenRouter)");
4789
- console.log(" 3. Codex CLI");
4855
+ console.log(" 1. Локальные модели");
4856
+ console.log(" 2. Российские AI (YandexGPT/GigaChat)");
4857
+ console.log(" 3. API (OpenAI/OpenRouter)");
4858
+ console.log(" 4. Codex CLI");
4790
4859
  console.log(" 0. Отмена");
4791
4860
 
4792
4861
  const answer = await askText("Номер: ");
4793
- return { 1: "local", 2: "api", 3: "codex" }[answer.trim()] || "";
4862
+ return { 1: "local", 2: "russian", 3: "api", 4: "codex" }[answer.trim()] || "";
4794
4863
  }
4795
4864
 
4796
4865
  async function openModelTargetMenu(target) {
4797
4866
  if (target === "local") {
4798
- const model = await chooseAiModel("iola");
4799
- if (model) await switchModelTarget("local", model);
4867
+ const selection = await chooseLocalModel();
4868
+ if (selection?.model) await switchModelTarget(selection.provider, selection.model);
4800
4869
  return;
4801
4870
  }
4802
4871
 
@@ -4812,6 +4881,20 @@ async function openModelTargetMenu(target) {
4812
4881
  return;
4813
4882
  }
4814
4883
 
4884
+ if (target === "yandexgpt" || target === "gigachat") {
4885
+ const model = await chooseAiModel(target);
4886
+ if (model) await switchModelTarget(target, model);
4887
+ return;
4888
+ }
4889
+
4890
+ if (target === "russian") {
4891
+ const provider = await chooseRussianProvider();
4892
+ if (!provider) return;
4893
+ const model = await chooseAiModel(provider);
4894
+ if (model) await switchModelTarget(provider, model);
4895
+ return;
4896
+ }
4897
+
4815
4898
  const provider = await chooseApiProvider();
4816
4899
  if (!provider) return;
4817
4900
  const model = await chooseAiModel(provider);
@@ -4837,6 +4920,25 @@ async function chooseApiProvider() {
4837
4920
  return choices[answer - 1]?.id || "";
4838
4921
  }
4839
4922
 
4923
+ async function chooseRussianProvider() {
4924
+ const config = await loadConfig();
4925
+ const russianProfiles = Object.entries(config.ai.profiles || {})
4926
+ .filter(([, profile]) => profile.provider === "yandexgpt" || profile.provider === "gigachat")
4927
+ .map(([name, profile]) => ({ id: profile.provider, label: `${name}: ${profile.provider} (${profile.model || "-"})` }));
4928
+ const choices = [
4929
+ ...russianProfiles,
4930
+ { id: "yandexgpt", label: "YandexGPT API" },
4931
+ { id: "gigachat", label: "GigaChat API" },
4932
+ ].filter((item, index, array) => array.findIndex((candidate) => candidate.id === item.id) === index);
4933
+
4934
+ console.log("Выберите российское AI-подключение:");
4935
+ choices.forEach((item, index) => console.log(` ${index + 1}. ${item.label}`));
4936
+ console.log(" 0. Отмена");
4937
+
4938
+ const answer = Number(await askText("Номер: "));
4939
+ return choices[answer - 1]?.id || "";
4940
+ }
4941
+
4840
4942
  async function getDefaultApiProviderForModelSwitch() {
4841
4943
  const config = await loadConfig();
4842
4944
  const activeProfile = config.ai.profiles?.[getActiveProfileName(config)];
@@ -4845,12 +4947,43 @@ async function getDefaultApiProviderForModelSwitch() {
4845
4947
  return apiProfile?.provider || "openai";
4846
4948
  }
4847
4949
 
4950
+ async function chooseLocalModel() {
4951
+ const models = await listAiModels("ollama");
4952
+ const choices = [
4953
+ { id: IOLA_LOCAL_MODEL, provider: "iola", label: `${IOLA_LOCAL_MODEL} - IOLA local router` },
4954
+ ...models
4955
+ .filter((model) => model.id !== IOLA_LOCAL_MODEL)
4956
+ .map((model) => ({
4957
+ id: model.id,
4958
+ provider: "ollama",
4959
+ label: `${model.id}${model.note ? ` - ${model.note}` : ""}`,
4960
+ })),
4961
+ { id: "__manual__", provider: "ollama", label: "Другая Ollama-модель: ввести имя вручную" },
4962
+ ].filter((item, index, array) => array.findIndex((candidate) => candidate.id === item.id) === index);
4963
+
4964
+ console.log("Выберите локальную модель:");
4965
+ choices.forEach((choice, index) => console.log(` ${index + 1}. ${choice.label}`));
4966
+ console.log(" 0. Отмена");
4967
+
4968
+ const answer = Number(await askText("Номер: "));
4969
+ const selected = choices[answer - 1];
4970
+ if (!selected) return null;
4971
+
4972
+ if (selected.id === "__manual__") {
4973
+ const model = (await askText("Имя Ollama-модели, например qwen3:4b: ")).trim();
4974
+ if (!model) return null;
4975
+ return { provider: "ollama", model };
4976
+ }
4977
+
4978
+ return { provider: selected.provider, model: selected.id };
4979
+ }
4980
+
4848
4981
  async function chooseAiModel(provider) {
4849
4982
  if (provider === "openrouter") {
4850
4983
  return chooseOpenRouterModel();
4851
4984
  }
4852
4985
 
4853
- if (provider === "openai") {
4986
+ if (provider === "openai" || provider === "yandexgpt" || provider === "gigachat") {
4854
4987
  const ready = await ensureApiKeyForModelSelection(provider);
4855
4988
  if (!ready) return "";
4856
4989
  }
@@ -4955,9 +5088,14 @@ async function chooseOpenRouterModel() {
4955
5088
  }
4956
5089
 
4957
5090
  async function ensureApiKeyForModelSelection(provider) {
4958
- if (provider !== "openai" && provider !== "openrouter") return true;
4959
- if (await getApiKey(provider)) return true;
4960
- const label = provider === "openai" ? "OpenAI" : "OpenRouter";
5091
+ if (!["openai", "openrouter", "yandexgpt", "gigachat"].includes(provider)) return true;
5092
+ if (await getApiKey(provider) && (provider !== "yandexgpt" || await getYandexFolderId())) return true;
5093
+ const label = {
5094
+ openai: "OpenAI",
5095
+ openrouter: "OpenRouter",
5096
+ yandexgpt: "YandexGPT",
5097
+ gigachat: "GigaChat",
5098
+ }[provider];
4961
5099
  console.log(`${label} API key не найден. Введите ключ, чтобы получить список моделей.`);
4962
5100
  try {
4963
5101
  await setAiKey(provider);
@@ -4988,7 +5126,9 @@ async function switchModelTarget(target, model) {
4988
5126
  if (!ready) return;
4989
5127
  }
4990
5128
  const profileName = provider === "ollama" || provider === "iola" ? "local" : provider;
4991
- const currentProfile = config.ai.profiles?.[profileName] || buildProfileFromOptions(provider, { model });
5129
+ const currentProfile = (provider === "ollama" || provider === "iola")
5130
+ ? buildProfileFromOptions(provider, { model })
5131
+ : (config.ai.profiles?.[profileName] || buildProfileFromOptions(provider, { model }));
4992
5132
  const profile = {
4993
5133
  ...currentProfile,
4994
5134
  provider,
@@ -5081,7 +5221,12 @@ async function setAiKey(provider) {
5081
5221
  throw new Error("Для сохранения ключа запустите команду в интерактивном терминале.");
5082
5222
  }
5083
5223
 
5084
- const envName = provider === "openai" ? "OPENAI_API_KEY" : "OPENROUTER_API_KEY";
5224
+ const envName = {
5225
+ openai: "OPENAI_API_KEY",
5226
+ openrouter: "OPENROUTER_API_KEY",
5227
+ yandexgpt: "YANDEXGPT_API_KEY",
5228
+ gigachat: "GIGACHAT_AUTH_KEY",
5229
+ }[provider];
5085
5230
  const key = (await askText(`Введите ${envName}: `)).trim();
5086
5231
 
5087
5232
  if (!key) {
@@ -5089,23 +5234,44 @@ async function setAiKey(provider) {
5089
5234
  }
5090
5235
 
5091
5236
  const secrets = await loadSecrets();
5092
- secrets[provider] = { apiKey: key };
5237
+ if (provider === "yandexgpt") {
5238
+ const folderId = (await askText("Введите YANDEXGPT_FOLDER_ID / ID каталога Yandex Cloud: ")).trim();
5239
+ if (!folderId) throw new Error("Folder ID пустой, сохранение отменено.");
5240
+ secrets[provider] = { apiKey: key, folderId };
5241
+ } else if (provider === "gigachat") {
5242
+ const scope = (await askText("Scope [GIGACHAT_API_PERS]: ")).trim() || "GIGACHAT_API_PERS";
5243
+ secrets[provider] = { apiKey: key, scope };
5244
+ } else {
5245
+ secrets[provider] = { apiKey: key };
5246
+ }
5093
5247
  await saveSecrets(secrets);
5094
5248
  console.log(`Ключ ${provider} сохранен локально: ${SECRETS_FILE}`);
5095
5249
  }
5096
5250
 
5097
5251
  async function printAiKeyStatus() {
5098
5252
  const secrets = await loadSecrets();
5099
- const rows = ["openai", "openrouter"].map((provider) => ({
5100
- provider,
5101
- env: provider === "openai" ? (process.env.OPENAI_API_KEY ? "yes" : "no") : (process.env.OPENROUTER_API_KEY ? "yes" : "no"),
5102
- local: secrets[provider]?.apiKey ? "yes" : "no",
5103
- }));
5253
+ const rows = ["yandexgpt", "gigachat", "openai", "openrouter"].map((provider) => {
5254
+ const env = {
5255
+ openai: process.env.OPENAI_API_KEY,
5256
+ openrouter: process.env.OPENROUTER_API_KEY,
5257
+ yandexgpt: process.env.YANDEXGPT_API_KEY || process.env.YANDEX_CLOUD_API_KEY,
5258
+ gigachat: process.env.GIGACHAT_AUTH_KEY || process.env.GIGACHAT_API_KEY,
5259
+ }[provider];
5260
+ return {
5261
+ provider,
5262
+ env: env ? "yes" : "no",
5263
+ local: secrets[provider]?.apiKey ? "yes" : "no",
5264
+ extra: provider === "yandexgpt"
5265
+ ? ((process.env.YANDEXGPT_FOLDER_ID || process.env.YANDEX_CLOUD_FOLDER_ID || secrets.yandexgpt?.folderId) ? "folder ok" : "folder missing")
5266
+ : (provider === "gigachat" && secrets.gigachat?.scope ? `scope ${secrets.gigachat.scope}` : ""),
5267
+ };
5268
+ });
5104
5269
 
5105
5270
  printTable(rows, [
5106
5271
  ["provider", "Провайдер"],
5107
5272
  ["env", "Env"],
5108
5273
  ["local", "Локально"],
5274
+ ["extra", "Дополнительно"],
5109
5275
  ]);
5110
5276
  }
5111
5277
 
@@ -6360,26 +6526,30 @@ function toCsv(rows) {
6360
6526
  }
6361
6527
 
6362
6528
  function assertKeyProvider(provider) {
6363
- if (provider !== "openai" && provider !== "openrouter") {
6364
- throw new Error("Провайдер должен быть openai или openrouter.");
6529
+ if (!["openai", "openrouter", "yandexgpt", "gigachat"].includes(provider)) {
6530
+ throw new Error("Провайдер должен быть yandexgpt, gigachat, openai или openrouter.");
6365
6531
  }
6366
6532
  }
6367
6533
 
6368
6534
  async function chooseAiProvider() {
6369
6535
  console.log("Выберите режим AI:");
6370
6536
  console.log("1. Локальная модель IOLA");
6371
- console.log("2. OpenAI API");
6372
- console.log("3. OpenRouter API");
6373
- console.log("4. Codex/MCP");
6374
- console.log("5. Ollama");
6537
+ console.log("2. Ollama");
6538
+ console.log("3. YandexGPT API");
6539
+ console.log("4. GigaChat API");
6540
+ console.log("5. OpenAI API");
6541
+ console.log("6. OpenRouter API");
6542
+ console.log("7. Codex/MCP");
6375
6543
 
6376
6544
  const answer = (await askText("Введите номер [1]: ")).trim() || "1";
6377
6545
  return {
6378
6546
  1: "iola",
6379
- 2: "openai",
6380
- 3: "openrouter",
6381
- 4: "codex",
6382
- 5: "ollama",
6547
+ 2: "ollama",
6548
+ 3: "yandexgpt",
6549
+ 4: "gigachat",
6550
+ 5: "openai",
6551
+ 6: "openrouter",
6552
+ 7: "codex",
6383
6553
  }[answer] || "iola";
6384
6554
  }
6385
6555
 
@@ -7967,6 +8137,14 @@ async function callAiProvider(config, messages) {
7967
8137
  return callOpenAiCompatible(config, messages, await getApiKey("openrouter"), "OpenRouter");
7968
8138
  }
7969
8139
 
8140
+ if (config.provider === "yandexgpt") {
8141
+ return callYandexGpt(config, messages);
8142
+ }
8143
+
8144
+ if (config.provider === "gigachat") {
8145
+ return callGigaChat(config, messages);
8146
+ }
8147
+
7970
8148
  if (config.provider === "codex") {
7971
8149
  return callCodex(config, messages);
7972
8150
  }
@@ -8444,6 +8622,96 @@ async function callAiRelayModels(config, apiKey, providerName) {
8444
8622
  return response.json();
8445
8623
  }
8446
8624
 
8625
+ async function callYandexGpt(config, messages) {
8626
+ const apiKey = await getApiKey("yandexgpt");
8627
+ const folderId = await getYandexFolderId();
8628
+ if (!apiKey || !folderId) {
8629
+ throw new Error("YandexGPT API key или folder ID не найден. Выполните iola ai key set yandexgpt или задайте YANDEXGPT_API_KEY и YANDEXGPT_FOLDER_ID.");
8630
+ }
8631
+
8632
+ const model = config.model || "yandexgpt-lite/latest";
8633
+ const modelUri = model.startsWith("gpt://") ? model : `gpt://${folderId}/${model}`;
8634
+ const response = await fetch(`${String(config.baseUrl || "https://llm.api.cloud.yandex.net/foundationModels/v1").replace(/\/+$/, "")}/completion`, {
8635
+ method: "POST",
8636
+ headers: {
8637
+ authorization: `Api-Key ${apiKey}`,
8638
+ "content-type": "application/json",
8639
+ },
8640
+ body: JSON.stringify({
8641
+ modelUri,
8642
+ completionOptions: {
8643
+ stream: false,
8644
+ temperature: Number(config.temperature ?? 0.2),
8645
+ maxTokens: String(config.maxTokens || 2000),
8646
+ },
8647
+ messages: messages.map((message) => ({
8648
+ role: message.role === "assistant" ? "assistant" : message.role === "system" ? "system" : "user",
8649
+ text: message.content,
8650
+ })),
8651
+ }),
8652
+ });
8653
+
8654
+ if (!response.ok) {
8655
+ const text = await response.text();
8656
+ throw new Error(`YandexGPT request failed: ${response.status} ${response.statusText}\n${sanitizeSecretFromText(text, apiKey)}`);
8657
+ }
8658
+
8659
+ const payload = await response.json();
8660
+ return payload.result?.alternatives?.[0]?.message?.text || "";
8661
+ }
8662
+
8663
+ async function callGigaChat(config, messages) {
8664
+ const authKey = await getApiKey("gigachat");
8665
+ if (!authKey) {
8666
+ throw new Error("GigaChat authorization key не найден. Выполните iola ai key set gigachat или задайте GIGACHAT_AUTH_KEY.");
8667
+ }
8668
+
8669
+ const token = await getGigaChatAccessToken(config, authKey);
8670
+ const response = await fetch(`${String(config.baseUrl || "https://gigachat.devices.sberbank.ru/api/v1").replace(/\/+$/, "")}/chat/completions`, {
8671
+ method: "POST",
8672
+ headers: {
8673
+ authorization: `Bearer ${token}`,
8674
+ "content-type": "application/json",
8675
+ },
8676
+ body: JSON.stringify({
8677
+ model: config.model || "GigaChat-2",
8678
+ messages,
8679
+ temperature: Number(config.temperature ?? 0.2),
8680
+ }),
8681
+ });
8682
+
8683
+ if (!response.ok) {
8684
+ const text = await response.text();
8685
+ throw new Error(`GigaChat request failed: ${response.status} ${response.statusText}\n${sanitizeSecretFromText(text, token)}`);
8686
+ }
8687
+
8688
+ const payload = await response.json();
8689
+ return payload.choices?.[0]?.message?.content || "";
8690
+ }
8691
+
8692
+ async function getGigaChatAccessToken(config, authKey) {
8693
+ const secrets = await loadSecrets();
8694
+ const scope = process.env.GIGACHAT_SCOPE || secrets.gigachat?.scope || config.scope || "GIGACHAT_API_PERS";
8695
+ const response = await fetch(config.authUrl || "https://ngw.devices.sberbank.ru:9443/api/v2/oauth", {
8696
+ method: "POST",
8697
+ headers: {
8698
+ authorization: `Basic ${authKey}`,
8699
+ "content-type": "application/x-www-form-urlencoded",
8700
+ RqUID: randomUUID(),
8701
+ },
8702
+ body: new URLSearchParams({ scope }).toString(),
8703
+ });
8704
+
8705
+ if (!response.ok) {
8706
+ const text = await response.text();
8707
+ throw new Error(`GigaChat token request failed: ${response.status} ${response.statusText}\n${sanitizeSecretFromText(text, authKey)}`);
8708
+ }
8709
+
8710
+ const payload = await response.json();
8711
+ if (!payload.access_token) throw new Error("GigaChat не вернул access_token.");
8712
+ return payload.access_token;
8713
+ }
8714
+
8447
8715
  function getAiNetworkMode(config = {}) {
8448
8716
  return validateAiNetworkMode(AI_NETWORK_MODE || config.networkMode || "gateway");
8449
8717
  }
@@ -8474,10 +8742,26 @@ async function getApiKey(provider) {
8474
8742
  return process.env.OPENROUTER_API_KEY;
8475
8743
  }
8476
8744
 
8745
+ if (provider === "yandexgpt" && (process.env.YANDEXGPT_API_KEY || process.env.YANDEX_CLOUD_API_KEY)) {
8746
+ return process.env.YANDEXGPT_API_KEY || process.env.YANDEX_CLOUD_API_KEY;
8747
+ }
8748
+
8749
+ if (provider === "gigachat" && (process.env.GIGACHAT_AUTH_KEY || process.env.GIGACHAT_API_KEY)) {
8750
+ return process.env.GIGACHAT_AUTH_KEY || process.env.GIGACHAT_API_KEY;
8751
+ }
8752
+
8477
8753
  const secrets = await loadSecrets();
8478
8754
  return secrets[provider]?.apiKey || "";
8479
8755
  }
8480
8756
 
8757
+ async function getYandexFolderId() {
8758
+ if (process.env.YANDEXGPT_FOLDER_ID || process.env.YANDEX_CLOUD_FOLDER_ID) {
8759
+ return process.env.YANDEXGPT_FOLDER_ID || process.env.YANDEX_CLOUD_FOLDER_ID;
8760
+ }
8761
+ const secrets = await loadSecrets();
8762
+ return secrets.yandexgpt?.folderId || "";
8763
+ }
8764
+
8481
8765
  async function listLayers(args) {
8482
8766
  const options = parseOptions(args);
8483
8767
  const info = await fetchJson(`${await getMcpBaseUrl()}/mcp-version`);
@@ -8735,6 +9019,20 @@ async function onboard(args = []) {
8735
9019
  await chooseAndSaveApiModel("openrouter");
8736
9020
  }
8737
9021
  }
9022
+ if (components.includes("yandexgpt")) {
9023
+ await aiSetup(["yandexgpt"]);
9024
+ if (process.stdin.isTTY) {
9025
+ await setAiKey("yandexgpt");
9026
+ await chooseAndSaveApiModel("yandexgpt");
9027
+ }
9028
+ }
9029
+ if (components.includes("gigachat")) {
9030
+ await aiSetup(["gigachat"]);
9031
+ if (process.stdin.isTTY) {
9032
+ await setAiKey("gigachat");
9033
+ await chooseAndSaveApiModel("gigachat");
9034
+ }
9035
+ }
8738
9036
  if (components.includes("codex")) {
8739
9037
  await installCodexIfMissing();
8740
9038
  await aiSetup(["codex"]);
@@ -8773,14 +9071,16 @@ async function chooseOnboardComponents(status = null) {
8773
9071
  1: "workspace",
8774
9072
  2: "policy",
8775
9073
  3: "iola",
8776
- 4: "openai",
8777
- 5: "openrouter",
8778
- 6: "codex",
8779
- 7: "codex-mcp",
8780
- 8: "archive",
8781
- 9: "index",
8782
- 10: "browser",
8783
- 11: "ollama",
9074
+ 4: "yandexgpt",
9075
+ 5: "gigachat",
9076
+ 6: "openai",
9077
+ 7: "openrouter",
9078
+ 8: "codex",
9079
+ 9: "codex-mcp",
9080
+ 10: "archive",
9081
+ 11: "index",
9082
+ 12: "browser",
9083
+ 13: "ollama",
8784
9084
  };
8785
9085
  return [...selected].map((item) => map[item] || item).filter(Boolean);
8786
9086
  } finally {
@@ -8808,6 +9108,8 @@ async function getOnboardComponentStatus() {
8808
9108
  policy: policyReady,
8809
9109
  iola: Boolean(readiness.iola),
8810
9110
  ollama: Boolean(ollamaVersion && readiness.ollama),
9111
+ yandexgpt: Boolean(readiness.yandexgpt),
9112
+ gigachat: Boolean(readiness.gigachat),
8811
9113
  openai: Boolean(readiness.openai),
8812
9114
  openrouter: Boolean(readiness.openrouter),
8813
9115
  codex: Boolean(codexVersion !== "не найден" && readiness.codex),
@@ -8823,14 +9125,16 @@ function onboardComponentRows(status) {
8823
9125
  ["1", "workspace", "workspace и контекст", "рабочая папка, IOLA.md и .iola/context.md"],
8824
9126
  ["2", "policy", "policy analyst", "разрешения и профиль аналитика"],
8825
9127
  ["3", "iola", "IOLA локальная модель", "локальная модель найдена"],
8826
- ["4", "openai", "OpenAI API", "API-ключ сохранен или есть в env"],
8827
- ["5", "openrouter", "OpenRouter API", "API-ключ сохранен или есть в env"],
8828
- ["6", "codex", "Codex CLI", "CLI установлен и авторизация найдена"],
8829
- ["7", "codex-mcp", "MCP для Codex", "можно переустановить/обновить"],
8830
- ["8", "archive", "7-Zip / архивы", "архиватор найден"],
8831
- ["9", "index", "Индекс локальных документов", "настраивается под выбранную папку"],
8832
- ["10", "browser", "Browser runtime", "Playwright/Chromium установлен"],
8833
- ["11", "ollama", "Ollama", "опциональный локальный runtime"],
9128
+ ["4", "yandexgpt", "YandexGPT API", "ключ и folder ID сохранены или есть в env"],
9129
+ ["5", "gigachat", "GigaChat API", "authorization key сохранен или есть в env"],
9130
+ ["6", "openai", "OpenAI API", "API-ключ сохранен или есть в env"],
9131
+ ["7", "openrouter", "OpenRouter API", "API-ключ сохранен или есть в env"],
9132
+ ["8", "codex", "Codex CLI", "CLI установлен и авторизация найдена"],
9133
+ ["9", "codex-mcp", "MCP для Codex", "можно переустановить/обновить"],
9134
+ ["10", "archive", "7-Zip / архивы", "архиватор найден"],
9135
+ ["11", "index", "Индекс локальных документов", "настраивается под выбранную папку"],
9136
+ ["12", "browser", "Browser runtime", "Playwright/Chromium установлен"],
9137
+ ["13", "ollama", "Ollama", "опциональный локальный runtime"],
8834
9138
  ];
8835
9139
  return rows.map(([number, key, title, hint]) => ({ number, key, title, hint, status: status[key] ? "готово" : "не настроено" }));
8836
9140
  }
@@ -8840,12 +9144,12 @@ function defaultOnboardSelection(status) {
8840
9144
  if (!status.workspace) defaults.push("1");
8841
9145
  if (!status.policy) defaults.push("2");
8842
9146
  if (!status.iola) defaults.push("3");
8843
- if (!status.archive) defaults.push("8");
9147
+ if (!status.archive) defaults.push("10");
8844
9148
  return defaults.length ? defaults : ["1", "2"];
8845
9149
  }
8846
9150
 
8847
9151
  function defaultOnboardComponents(status) {
8848
- const map = { 1: "workspace", 2: "policy", 3: "iola", 4: "openai", 5: "openrouter", 6: "codex", 7: "codex-mcp", 8: "archive", 9: "index", 10: "browser", 11: "ollama" };
9152
+ const map = { 1: "workspace", 2: "policy", 3: "iola", 4: "yandexgpt", 5: "gigachat", 6: "openai", 7: "openrouter", 8: "codex", 9: "codex-mcp", 10: "archive", 11: "index", 12: "browser", 13: "ollama" };
8849
9153
  return defaultOnboardSelection(status).map((item) => map[item]).filter(Boolean);
8850
9154
  }
8851
9155
 
@@ -10821,6 +11125,9 @@ function sanitizeConfig(config) {
10821
11125
  if (profile?.provider === "openai" || profile?.provider === "openrouter") {
10822
11126
  profile.networkMode = profile.networkMode || "gateway";
10823
11127
  }
11128
+ if (profile?.provider === "yandexgpt" || profile?.provider === "gigachat") {
11129
+ profile.networkMode = profile.networkMode || "direct";
11130
+ }
10824
11131
  }
10825
11132
  return next;
10826
11133
  }
@@ -10833,7 +11140,7 @@ function validateConfig(config) {
10833
11140
  if (!config.ai?.profiles || typeof config.ai.profiles !== "object") errors.push("ai.profiles обязателен");
10834
11141
  if (config.ai?.activeProfile && !config.ai.profiles?.[config.ai.activeProfile]) errors.push(`ai.activeProfile не найден в profiles: ${config.ai.activeProfile}`);
10835
11142
  for (const [name, profile] of Object.entries(config.ai?.profiles || {})) {
10836
- if (!["iola", "ollama", "openai", "openrouter", "codex"].includes(profile.provider)) errors.push(`ai.profiles.${name}.provider неизвестен`);
11143
+ if (!["iola", "ollama", "yandexgpt", "gigachat", "openai", "openrouter", "codex"].includes(profile.provider)) errors.push(`ai.profiles.${name}.provider неизвестен`);
10837
11144
  if (profile.provider !== "codex" && profile.provider !== "iola" && !profile.baseUrl) errors.push(`ai.profiles.${name}.baseUrl обязателен`);
10838
11145
  if (profile.networkMode && !["direct", "gateway", "auto"].includes(profile.networkMode)) errors.push(`ai.profiles.${name}.networkMode должен быть direct, gateway или auto`);
10839
11146
  }
@@ -10852,7 +11159,7 @@ function configSchema() {
10852
11159
  required: ["api", "ai"],
10853
11160
  properties: {
10854
11161
  api: { required: ["baseUrl", "mcpBaseUrl"] },
10855
- ai: { required: ["activeProfile", "profiles"], providers: ["iola", "ollama", "openai", "openrouter", "codex"] },
11162
+ ai: { required: ["activeProfile", "profiles"], providers: ["iola", "ollama", "yandexgpt", "gigachat", "openai", "openrouter", "codex"] },
10856
11163
  permissions: { localTools: ALL_LOCAL_TOOLS, runtime: ["readFiles", "writeFiles", "editFiles", "deleteFiles", "sync", "externalApi", "externalAi", "codex"] },
10857
11164
  toolsets: { available: Object.keys(TOOLSETS) },
10858
11165
  files: { modes: ["locked", "read-only", "workspace-write", "full-access"], approvals: ["never", "on-write", "on-danger", "always"] },
@@ -55,6 +55,8 @@ assertIncludes(cliSource, "\\x1b[1m$1\\x1b[22m", "AI answer renderer should supp
55
55
  assertIncludes(cliSource, "ensureApiKeyForModelSelection", "API model selection should prompt for missing provider keys");
56
56
  assertIncludes(cliSource, "isOpenAiTextGenerationModel", "OpenAI model selection should filter technical and legacy models");
57
57
  assertIncludes(cliSource, "dedupeDatedOpenAiModels", "OpenAI model selection should hide dated duplicates when aliases exist");
58
+ assertIncludes(cliSource, "chooseLocalModel", "Local model selection should support IOLA and Ollama models");
59
+ assertIncludes(cliSource, "Другая Ollama-модель", "Local model selection should allow manual Ollama model names");
58
60
 
59
61
  const commands = await runCli(["commands"]);
60
62
  assertIncludes(commands, "iola browser status|install|open|text|html|screenshot|pdf|click|type|eval", "commands");
@@ -10,22 +10,103 @@ iola ai profile use local
10
10
  iola ask "найди школы на Петрова"
11
11
  ```
12
12
 
13
+ В интерактивном агенте команда `/model` открывает выбор локальной модели. В локальном меню доступны:
14
+
15
+ - штатная модель IOLA `iola-router:qwen3-1.7b-v4-q8`;
16
+ - установленные и рекомендуемые модели Ollama;
17
+ - ручной ввод имени любой Ollama-модели.
18
+
19
+ Если выбранная Ollama-модель еще не установлена, CLI предложит скачать ее через `ollama pull`. При выборе сторонней Ollama-модели профиль `local` сохраняется как `provider: ollama`; при выборе штатной модели IOLA профиль `local` возвращается к `provider: iola`.
20
+
21
+ ## Российские AI
22
+
23
+ Российские провайдеры вынесены в отдельный блок `/model` и вызываются напрямую, без gateway/proxy:
24
+
25
+ ```text
26
+ /model
27
+ 2. Российские AI (YandexGPT/GigaChat)
28
+ ```
29
+
30
+ ### YandexGPT
31
+
32
+ Официальные страницы:
33
+
34
+ - документация Foundation Models: `https://yandex.cloud/ru/docs/foundation-models/`;
35
+ - аутентификация API: `https://yandex.cloud/ru/docs/ai-studio/api-ref/authentication`;
36
+ - тарифы: `https://yandex.cloud/ru/docs/foundation-models/pricing`.
37
+
38
+ Для CLI нужны API key и ID каталога Yandex Cloud:
39
+
40
+ ```bash
41
+ iola ai key set yandexgpt
42
+ iola ai setup yandexgpt --model yandexgpt-lite/latest
43
+ ```
44
+
45
+ CLI также понимает env-переменные `YANDEXGPT_API_KEY` или `YANDEX_CLOUD_API_KEY`, а для каталога - `YANDEXGPT_FOLDER_ID` или `YANDEX_CLOUD_FOLDER_ID`.
46
+
47
+ ### GigaChat
48
+
49
+ Официальные страницы:
50
+
51
+ - документация GigaChat: `https://developers.sber.ru/docs/ru/gigachat/overview`;
52
+ - получение OAuth-токена: `https://developers.sber.ru/docs/ru/gigachat/api/reference/rest/post-token`;
53
+ - тарифы: `https://developers.sber.ru/docs/ru/gigachat/tariffs`.
54
+
55
+ Для CLI нужен authorization key:
56
+
57
+ ```bash
58
+ iola ai key set gigachat
59
+ iola ai setup gigachat --model GigaChat-2
60
+ ```
61
+
62
+ CLI также понимает env-переменные `GIGACHAT_AUTH_KEY` или `GIGACHAT_API_KEY`. По умолчанию используется scope `GIGACHAT_API_PERS`; при необходимости его можно задать через `GIGACHAT_SCOPE`.
63
+
64
+ По тарифам: у GigaChat для физических лиц есть Freemium-лимит на токены; для больших объемов используются платные пакеты. У YandexGPT тарификация идет через Yandex Cloud по токенам и квотам аккаунта, поэтому актуальные бесплатные гранты или лимиты нужно проверять в консоли Yandex Cloud.
65
+
13
66
  ## OpenAI
14
67
 
68
+ Получение ключа OpenAI Platform:
69
+
70
+ 1. Зарегистрируйтесь или войдите в OpenAI Platform: `https://platform.openai.com/`.
71
+ 2. Откройте страницу API-ключей: `https://platform.openai.com/api-keys`.
72
+ 3. Выберите нужный project или создайте новый project.
73
+ 4. Нажмите `Create new secret key`.
74
+ 5. Скопируйте ключ сразу после создания. Повторно посмотреть полный ключ обычно нельзя.
75
+ 6. В CLI сохраните ключ:
76
+
15
77
  ```bash
16
78
  iola ai key set openai
17
79
  iola ai setup openai --model gpt-4.1-mini
18
80
  iola ask "найди школу 29" --profile openai
19
81
  ```
20
82
 
83
+ Важно: OpenAI API Platform и ChatGPT Plus/Pro - разные продукты. Подписка ChatGPT не заменяет API-биллинг. Для работы API обычно нужно отдельно настроить billing в OpenAI Platform.
84
+
21
85
  ## OpenRouter
22
86
 
87
+ Получение ключа OpenRouter:
88
+
89
+ 1. Зарегистрируйтесь или войдите в OpenRouter: `https://openrouter.ai/`.
90
+ 2. Откройте страницу ключей: `https://openrouter.ai/settings/keys`.
91
+ 3. Нажмите создание нового API key.
92
+ 4. Задайте имя ключа и при необходимости лимит расходов.
93
+ 5. Скопируйте ключ сразу после создания. Храните его как секрет.
94
+ 6. В CLI сохраните ключ:
95
+
23
96
  ```bash
24
97
  iola ai key set openrouter
25
98
  iola ai setup openrouter --model openai/gpt-4.1-mini
26
99
  iola ai models openrouter --search qwen
27
100
  ```
28
101
 
102
+ OpenRouter удобен тем, что через один ключ можно выбирать модели разных разработчиков: OpenAI, Anthropic, Google, Qwen / Alibaba, DeepSeek, Meta / Llama, Mistral AI и других.
103
+
104
+ ## Оплата
105
+
106
+ Важно: оплата российскими банковскими картами для OpenAI Platform и OpenRouter может быть невозможна. Перед настройкой платных API заранее проверьте доступный способ оплаты и пополнения баланса в личном кабинете выбранного сервиса.
107
+
108
+ Ключи YandexGPT, GigaChat, OpenAI и OpenRouter сохраняются локально на устройстве пользователя в `~/.iola/secrets.json`. CLI не публикует ключи в репозиторий и не записывает их в документацию.
109
+
29
110
  В интерактивном CLI модели удобнее выбирать через slash-команду:
30
111
 
31
112
  ```text
@@ -56,4 +137,4 @@ iola ai profile use local
56
137
  iola ai profile use openrouter
57
138
  ```
58
139
 
59
- В интерактивном агенте можно использовать `/model`, чтобы выбрать подключение и модель без ручного ввода id модели.
140
+ В интерактивном агенте можно использовать `/model`, чтобы выбрать подключение и модель без ручного ввода id модели. Порядок меню: локальные модели, российские AI, зарубежные API, Codex CLI.
package/wiki/Home.md CHANGED
@@ -10,7 +10,7 @@
10
10
  - проверка качества данных;
11
11
  - выгрузка CSV/JSON;
12
12
  - работа с локальной моделью Ollama;
13
- - работа с OpenAI, OpenRouter и Codex CLI;
13
+ - работа с YandexGPT, GigaChat, OpenAI, OpenRouter и Codex CLI;
14
14
  - подключение публичного MCP-сервера.
15
15
 
16
16
  Быстрый старт:
@@ -28,33 +28,47 @@ iola master
28
28
 
29
29
  Если модель уже скачана и доступна, пункт показывается как `готово`.
30
30
 
31
- ### 4. OpenAI API
31
+ После настройки локальную модель можно менять в интерактивном агенте через `/model`. Помимо штатной IOLA-модели можно выбрать установленную или рекомендуемую Ollama-модель, либо вручную ввести имя любой модели из библиотеки Ollama.
32
+
33
+ ### 4. YandexGPT API
34
+
35
+ Настраивает профиль YandexGPT и сохраняет API key плюс folder ID локально у пользователя.
36
+
37
+ Российский провайдер вызывается напрямую, без gateway/proxy.
38
+
39
+ ### 5. GigaChat API
40
+
41
+ Настраивает профиль GigaChat и сохраняет authorization key локально у пользователя.
42
+
43
+ Российский провайдер вызывается напрямую, без gateway/proxy.
44
+
45
+ ### 6. OpenAI API
32
46
 
33
47
  Настраивает профиль OpenAI и сохраняет API-ключ локально у пользователя.
34
48
 
35
49
  После сохранения ключа мастер предлагает выбрать модель из доступного списка.
36
50
 
37
- ### 5. OpenRouter API
51
+ ### 7. OpenRouter API
38
52
 
39
53
  Настраивает профиль OpenRouter и сохраняет API-ключ локально у пользователя.
40
54
 
41
55
  После сохранения ключа мастер предлагает выбрать разработчика моделей OpenRouter, а затем одну из свежих моделей для текстовой работы. В списке моделей `0` возвращает к выбору разработчика.
42
56
 
43
- ### 6. Codex CLI
57
+ ### 8. Codex CLI
44
58
 
45
59
  Проверяет наличие Codex CLI и авторизации. Если Codex уже установлен и вход выполнен, пункт показывается как `готово`.
46
60
 
47
- ### 7. MCP для Codex
61
+ ### 9. MCP для Codex
48
62
 
49
63
  Добавляет MCP-сервер открытых данных Йошкар-Олы в Codex. Этот пункт можно запускать повторно для обновления подключения.
50
64
 
51
- ### 8. 7-Zip / архивы
65
+ ### 10. 7-Zip / архивы
52
66
 
53
67
  Проверяет 7-Zip. Если архиватор не найден, мастер устанавливает его.
54
68
 
55
69
  Нужен для чтения, распаковки, сборки и индексирования архивов.
56
70
 
57
- ### 9. Индекс локальных документов
71
+ ### 11. Индекс локальных документов
58
72
 
59
73
  Готовит режим чтения файлов и подсказывает команду индексирования локальной папки:
60
74
 
@@ -62,10 +76,14 @@ iola master
62
76
  iola index folder ./docs
63
77
  ```
64
78
 
65
- ### 10. Browser runtime
79
+ ### 12. Browser runtime
66
80
 
67
81
  Проверяет Playwright/Chromium runtime. Нужен для браузерного агента.
68
82
 
83
+ ### 13. Ollama
84
+
85
+ Опциональный локальный runtime для выбора сторонних моделей из библиотеки Ollama.
86
+
69
87
  Если runtime уже установлен, пункт показывается как `готово`.
70
88
 
71
89
  ## Повторный запуск
@@ -24,14 +24,27 @@ ollama serve
24
24
  iola ai doctor
25
25
  ```
26
26
 
27
- ## OpenAI/OpenRouter key не найден
27
+ ## YandexGPT/GigaChat/OpenAI/OpenRouter key не найден
28
28
 
29
29
  ```bash
30
30
  iola ai key status
31
+ iola ai key set yandexgpt
32
+ iola ai key set gigachat
31
33
  iola ai key set openai
32
34
  iola ai key set openrouter
33
35
  ```
34
36
 
37
+ Где получить ключ:
38
+
39
+ - YandexGPT: `https://yandex.cloud/ru/docs/ai-studio/api-ref/authentication`
40
+ - GigaChat: `https://developers.sber.ru/docs/ru/gigachat/overview`
41
+ - OpenAI Platform: `https://platform.openai.com/api-keys`
42
+ - OpenRouter: `https://openrouter.ai/settings/keys`
43
+
44
+ Для YandexGPT нужен не только API key, но и folder ID каталога Yandex Cloud. Для GigaChat нужен authorization key; CLI сам получает OAuth-токен перед запросом.
45
+
46
+ Важно: оплата российскими банковскими картами для OpenAI Platform и OpenRouter может быть невозможна. Если API-запросы не проходят после сохранения ключа, проверьте не только ключ, но и billing/баланс в личном кабинете сервиса.
47
+
35
48
  ## Нет локальных данных
36
49
 
37
50
  ```bash