@iola_adm/iola-cli 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -2
- package/bin/iola.js +0 -0
- package/package.json +1 -1
- package/src/cli.js +477 -67
- package/test/smoke-test.js +14 -0
- package/wiki/AI-/320/277/321/200/320/276/321/204/320/270/320/273/320/270.md +160 -1
- package/wiki/Home.md +1 -1
- package/wiki//320/234/320/260/321/201/321/202/320/265/321/200-/320/275/320/260/321/201/321/202/321/200/320/276/320/271/320/272/320/270.md +23 -7
- package/wiki//320/240/320/265/321/210/320/265/320/275/320/270/320/265-/320/277/321/200/320/276/320/261/320/273/320/265/320/274.md +14 -1
package/README.md
CHANGED
|
@@ -131,7 +131,7 @@ CLI использует модель `iola-router:qwen3-1.7b-v4-q8` из GGUF-
|
|
|
131
131
|
/model
|
|
132
132
|
```
|
|
133
133
|
|
|
134
|
-
В интерактивном CLI команда `/model` переключает
|
|
134
|
+
В интерактивном CLI команда `/model` переключает локальные модели, российские AI-провайдеры YandexGPT/GigaChat, API-профили OpenAI/OpenRouter и Codex CLI. Российские провайдеры вызываются напрямую, без gateway/proxy. Для OpenRouter выбор устроен так: сначала выбирается разработчик моделей, затем CLI показывает до 30 самых свежих моделей для текстовой работы с датой релиза и размером контекста. В списке моделей `0` возвращает к выбору разработчика.
|
|
135
135
|
|
|
136
136
|
В локальном выборе доступны:
|
|
137
137
|
|
|
@@ -141,6 +141,28 @@ CLI использует модель `iola-router:qwen3-1.7b-v4-q8` из GGUF-
|
|
|
141
141
|
|
|
142
142
|
Если выбранная Ollama-модель еще не скачана, CLI предложит выполнить `ollama pull`.
|
|
143
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 проверьте доступный способ оплаты в личном кабинете сервиса.
|
|
165
|
+
|
|
144
166
|
Ollama остается опциональным runtime:
|
|
145
167
|
|
|
146
168
|
```bash
|
|
@@ -178,7 +200,7 @@ iola version --check
|
|
|
178
200
|
- интеграция с публичным MCP-сервером Йошкар-Олы;
|
|
179
201
|
- поиск и выгрузка открытых данных;
|
|
180
202
|
- локальная SQLite-БД, история, сессии и FTS-поиск;
|
|
181
|
-
- AI-профили для IOLA local, Ollama, OpenAI, OpenRouter и Codex CLI;
|
|
203
|
+
- AI-профили для IOLA local, Ollama, YandexGPT, GigaChat, OpenAI, OpenRouter и Codex CLI;
|
|
182
204
|
- локальный tool-agent для модели IOLA с tools `search_data`, `search_entities`, `resolve_entity_field`, `get_card`, `export_report`, `file_read`, `browser_open`;
|
|
183
205
|
- ленивые skills, toolsets, permissions, memory, hooks и готовые agents;
|
|
184
206
|
- subagents, skill bundles, layered settings, usage/budget accounting и trajectory export;
|
package/bin/iola.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
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", "
|
|
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 ||
|
|
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
|
-
|
|
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 (
|
|
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,13 +4852,14 @@ function normalizeModelMenuTarget(value = "") {
|
|
|
4784
4852
|
|
|
4785
4853
|
async function chooseModelTarget() {
|
|
4786
4854
|
console.log("Выберите AI-подключение:");
|
|
4787
|
-
console.log(" 1.
|
|
4788
|
-
console.log(" 2.
|
|
4789
|
-
console.log(" 3.
|
|
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: "
|
|
4862
|
+
return { 1: "local", 2: "russian", 3: "api", 4: "codex" }[answer.trim()] || "";
|
|
4794
4863
|
}
|
|
4795
4864
|
|
|
4796
4865
|
async function openModelTargetMenu(target) {
|
|
@@ -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)];
|
|
@@ -4881,7 +4983,7 @@ async function chooseAiModel(provider) {
|
|
|
4881
4983
|
return chooseOpenRouterModel();
|
|
4882
4984
|
}
|
|
4883
4985
|
|
|
4884
|
-
if (provider === "openai") {
|
|
4986
|
+
if (provider === "openai" || provider === "yandexgpt" || provider === "gigachat") {
|
|
4885
4987
|
const ready = await ensureApiKeyForModelSelection(provider);
|
|
4886
4988
|
if (!ready) return "";
|
|
4887
4989
|
}
|
|
@@ -4986,9 +5088,14 @@ async function chooseOpenRouterModel() {
|
|
|
4986
5088
|
}
|
|
4987
5089
|
|
|
4988
5090
|
async function ensureApiKeyForModelSelection(provider) {
|
|
4989
|
-
if (
|
|
4990
|
-
if (await getApiKey(provider)) return true;
|
|
4991
|
-
const label =
|
|
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];
|
|
4992
5099
|
console.log(`${label} API key не найден. Введите ключ, чтобы получить список моделей.`);
|
|
4993
5100
|
try {
|
|
4994
5101
|
await setAiKey(provider);
|
|
@@ -5114,7 +5221,12 @@ async function setAiKey(provider) {
|
|
|
5114
5221
|
throw new Error("Для сохранения ключа запустите команду в интерактивном терминале.");
|
|
5115
5222
|
}
|
|
5116
5223
|
|
|
5117
|
-
const envName =
|
|
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];
|
|
5118
5230
|
const key = (await askText(`Введите ${envName}: `)).trim();
|
|
5119
5231
|
|
|
5120
5232
|
if (!key) {
|
|
@@ -5122,23 +5234,44 @@ async function setAiKey(provider) {
|
|
|
5122
5234
|
}
|
|
5123
5235
|
|
|
5124
5236
|
const secrets = await loadSecrets();
|
|
5125
|
-
|
|
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
|
+
}
|
|
5126
5247
|
await saveSecrets(secrets);
|
|
5127
5248
|
console.log(`Ключ ${provider} сохранен локально: ${SECRETS_FILE}`);
|
|
5128
5249
|
}
|
|
5129
5250
|
|
|
5130
5251
|
async function printAiKeyStatus() {
|
|
5131
5252
|
const secrets = await loadSecrets();
|
|
5132
|
-
const rows = ["openai", "openrouter"].map((provider) =>
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
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
|
+
});
|
|
5137
5269
|
|
|
5138
5270
|
printTable(rows, [
|
|
5139
5271
|
["provider", "Провайдер"],
|
|
5140
5272
|
["env", "Env"],
|
|
5141
5273
|
["local", "Локально"],
|
|
5274
|
+
["extra", "Дополнительно"],
|
|
5142
5275
|
]);
|
|
5143
5276
|
}
|
|
5144
5277
|
|
|
@@ -6393,26 +6526,30 @@ function toCsv(rows) {
|
|
|
6393
6526
|
}
|
|
6394
6527
|
|
|
6395
6528
|
function assertKeyProvider(provider) {
|
|
6396
|
-
if (
|
|
6397
|
-
throw new Error("Провайдер должен быть openai или openrouter.");
|
|
6529
|
+
if (!["openai", "openrouter", "yandexgpt", "gigachat"].includes(provider)) {
|
|
6530
|
+
throw new Error("Провайдер должен быть yandexgpt, gigachat, openai или openrouter.");
|
|
6398
6531
|
}
|
|
6399
6532
|
}
|
|
6400
6533
|
|
|
6401
6534
|
async function chooseAiProvider() {
|
|
6402
6535
|
console.log("Выберите режим AI:");
|
|
6403
6536
|
console.log("1. Локальная модель IOLA");
|
|
6404
|
-
console.log("2.
|
|
6405
|
-
console.log("3.
|
|
6406
|
-
console.log("4.
|
|
6407
|
-
console.log("5.
|
|
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");
|
|
6408
6543
|
|
|
6409
6544
|
const answer = (await askText("Введите номер [1]: ")).trim() || "1";
|
|
6410
6545
|
return {
|
|
6411
6546
|
1: "iola",
|
|
6412
|
-
2: "
|
|
6413
|
-
3: "
|
|
6414
|
-
4: "
|
|
6415
|
-
5: "
|
|
6547
|
+
2: "ollama",
|
|
6548
|
+
3: "yandexgpt",
|
|
6549
|
+
4: "gigachat",
|
|
6550
|
+
5: "openai",
|
|
6551
|
+
6: "openrouter",
|
|
6552
|
+
7: "codex",
|
|
6416
6553
|
}[answer] || "iola";
|
|
6417
6554
|
}
|
|
6418
6555
|
|
|
@@ -6558,7 +6695,7 @@ async function aiAsk(args, context = {}) {
|
|
|
6558
6695
|
const historyEnabled = !options.bare && !options["no-history"] && isFeatureEnabled("sqlite-history");
|
|
6559
6696
|
const sessionId = historyEnabled && isFeatureEnabled("sessions") ? ensureSessionForAsk(options, providerConfig, question) : null;
|
|
6560
6697
|
const history = context.history || (sessionId ? getSessionAiHistory(sessionId) : []);
|
|
6561
|
-
const directAnswer = buildDirectDataAnswer(question, dataContext);
|
|
6698
|
+
const directAnswer = await buildDirectDataAnswer(question, dataContext);
|
|
6562
6699
|
if (directAnswer) {
|
|
6563
6700
|
if (historyEnabled) {
|
|
6564
6701
|
recordAskHistory({ question, answer: directAnswer, providerConfig, dataContext, error: "", sessionId });
|
|
@@ -6621,10 +6758,12 @@ async function aiAsk(args, context = {}) {
|
|
|
6621
6758
|
return answer;
|
|
6622
6759
|
}
|
|
6623
6760
|
|
|
6624
|
-
function buildDirectDataAnswer(question, dataContext) {
|
|
6761
|
+
async function buildDirectDataAnswer(question, dataContext) {
|
|
6625
6762
|
const normalized = question.toLocaleLowerCase("ru-RU");
|
|
6626
6763
|
const requestedFields = detectDirectDataFields(normalized);
|
|
6627
6764
|
if (requestedFields.length === 0) return "";
|
|
6765
|
+
const educationAnswer = await buildDeterministicEducationAnswer(question, requestedFields);
|
|
6766
|
+
if (educationAnswer) return educationAnswer;
|
|
6628
6767
|
const rows = [
|
|
6629
6768
|
...dataContext.schools.map((item) => ({ layer: "schools", layerName: "школы", ...item })),
|
|
6630
6769
|
...dataContext.kindergartens.map((item) => ({ layer: "kindergartens", layerName: "детские сады", ...item })),
|
|
@@ -6654,6 +6793,140 @@ function detectDirectDataFields(normalizedQuestion) {
|
|
|
6654
6793
|
return [...new Set(fields)];
|
|
6655
6794
|
}
|
|
6656
6795
|
|
|
6796
|
+
async function buildDeterministicEducationAnswer(question, requestedFields) {
|
|
6797
|
+
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
6798
|
+
const layer = /(сад|детсад|детск\w*\s+сад|садик)/iu.test(normalized)
|
|
6799
|
+
? "kindergartens"
|
|
6800
|
+
: /(школ|сош|лице|гимнази)/iu.test(normalized)
|
|
6801
|
+
? "schools"
|
|
6802
|
+
: "";
|
|
6803
|
+
if (!layer) return "";
|
|
6804
|
+
|
|
6805
|
+
const requests = extractEducationFactRequests(question, requestedFields, layer);
|
|
6806
|
+
if (requests.length === 0) return "";
|
|
6807
|
+
|
|
6808
|
+
const items = normalizeItems(await fetchAllApiItems(`${await getApiBaseUrl()}/${DATASETS[layer].endpoint}`))
|
|
6809
|
+
.map((item) => ({ layer, layerName: layer === "schools" ? "школы" : "детские сады", ...selectPublicSummary(item) }));
|
|
6810
|
+
const answers = [];
|
|
6811
|
+
|
|
6812
|
+
for (const request of requests) {
|
|
6813
|
+
const answer = resolveEducationFactRequest(request, items, layer);
|
|
6814
|
+
if (answer) answers.push(answer);
|
|
6815
|
+
}
|
|
6816
|
+
|
|
6817
|
+
return answers.filter(Boolean).join("\n");
|
|
6818
|
+
}
|
|
6819
|
+
|
|
6820
|
+
function extractEducationFactRequests(question, requestedFields, layer) {
|
|
6821
|
+
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
6822
|
+
const segments = splitEducationQuestionSegments(question);
|
|
6823
|
+
const requests = [];
|
|
6824
|
+
|
|
6825
|
+
for (const segment of segments) {
|
|
6826
|
+
const segmentFields = detectDirectDataFields(segment.toLocaleLowerCase("ru-RU"));
|
|
6827
|
+
const fields = segmentFields.length > 0 ? segmentFields : (segments.length === 1 ? requestedFields : []);
|
|
6828
|
+
if (fields.length === 0) continue;
|
|
6829
|
+
const place = detectEducationPlace(segment) || detectEducationPlace(question);
|
|
6830
|
+
const number = extractEntityNumberFromQuestion(segment, layer)
|
|
6831
|
+
|| (segments.length === 1 ? extractEntityNumberFromQuestion(question, layer) : "");
|
|
6832
|
+
const hasEntitySignal = Boolean(number || place || /(школ|сош|лице|гимнази|сад|детсад|детск\w*\s+сад|садик)/iu.test(segment));
|
|
6833
|
+
if (!hasEntitySignal) continue;
|
|
6834
|
+
requests.push({ segment, fields, number, place });
|
|
6835
|
+
}
|
|
6836
|
+
|
|
6837
|
+
if (requests.length === 0 && requestedFields.length > 0 && /(школ|сош|лице|гимнази|сад|детсад|детск\w*\s+сад|садик)/iu.test(normalized)) {
|
|
6838
|
+
requests.push({
|
|
6839
|
+
segment: question,
|
|
6840
|
+
fields: requestedFields,
|
|
6841
|
+
number: extractEntityNumberFromQuestion(question, layer),
|
|
6842
|
+
place: detectEducationPlace(question),
|
|
6843
|
+
});
|
|
6844
|
+
}
|
|
6845
|
+
|
|
6846
|
+
return requests;
|
|
6847
|
+
}
|
|
6848
|
+
|
|
6849
|
+
function splitEducationQuestionSegments(question) {
|
|
6850
|
+
const text = String(question || "").trim();
|
|
6851
|
+
if (!text) return [];
|
|
6852
|
+
return text
|
|
6853
|
+
.split(/\s+(?:и|а также|,|;)\s+(?=(?:кто|какой|какая|адрес|телефон|инн|сайт|почт|email|директ|руковод|завед|где))/iu)
|
|
6854
|
+
.map((part) => part.trim())
|
|
6855
|
+
.filter(Boolean);
|
|
6856
|
+
}
|
|
6857
|
+
|
|
6858
|
+
function detectEducationPlace(text) {
|
|
6859
|
+
const normalized = normalizeEntityText(text || "");
|
|
6860
|
+
if (/(козьмодемьянск|козьмодемьянск[аеуом]?|козьмодемьянск\w*)/iu.test(normalized)) {
|
|
6861
|
+
return { id: "kozmodemyansk", label: "Козьмодемьянск", locative: "Козьмодемьянске", supported: false, aliases: ["козьмодемьянск", "козмодемьянск"] };
|
|
6862
|
+
}
|
|
6863
|
+
if (/(семеновк|семёновк)/iu.test(normalized)) {
|
|
6864
|
+
return { id: "semenovka", label: "Семёновка", locative: "Семёновке", supported: true, aliases: ["семеновк", "семёновк"] };
|
|
6865
|
+
}
|
|
6866
|
+
if (/(йошкар|йошкар-ола|йошкар ола)/iu.test(normalized)) {
|
|
6867
|
+
return { id: "yoshkar_ola", label: "Йошкар-Ола", locative: "Йошкар-Оле", supported: true, aliases: ["йошкар", "йошкар-ола", "йошкар ола"] };
|
|
6868
|
+
}
|
|
6869
|
+
return null;
|
|
6870
|
+
}
|
|
6871
|
+
|
|
6872
|
+
function resolveEducationFactRequest(request, items, layer) {
|
|
6873
|
+
const entityLabel = layer === "schools" ? "школу" : "детский сад";
|
|
6874
|
+
if (request.place && !request.place.supported) {
|
|
6875
|
+
return `В текущих открытых данных iola-cli есть данные городского округа Йошкар-Ола. Данных по ${request.place.locative || request.place.label} в этом слое нет, поэтому ответить по этому объекту не могу.`;
|
|
6876
|
+
}
|
|
6877
|
+
|
|
6878
|
+
let candidates = items;
|
|
6879
|
+
if (request.place) {
|
|
6880
|
+
candidates = candidates.filter((item) => itemMatchesPlace(item, request.place));
|
|
6881
|
+
}
|
|
6882
|
+
|
|
6883
|
+
if (request.number) {
|
|
6884
|
+
const exactByNumber = candidates.filter((item) => itemNameHasNumber(item, request.number));
|
|
6885
|
+
if (exactByNumber.length === 0) {
|
|
6886
|
+
if (request.place && candidates.length > 0) {
|
|
6887
|
+
return [
|
|
6888
|
+
`Точную ${entityLabel} № ${request.number} в ${request.place.locative || request.place.label} в открытом слое не нашел.`,
|
|
6889
|
+
`В ${request.place.locative || request.place.label} есть:`,
|
|
6890
|
+
...candidates.slice(0, 5).map((item) => `- ${getDirectDataItemName(item)}${item.address ? `; адрес: ${item.address}` : ""}${item.inn ? `; ИНН ${item.inn}` : ""}`),
|
|
6891
|
+
].join("\n");
|
|
6892
|
+
}
|
|
6893
|
+
return `В открытом слое не нашел ${entityLabel} № ${request.number}.`;
|
|
6894
|
+
}
|
|
6895
|
+
candidates = exactByNumber;
|
|
6896
|
+
}
|
|
6897
|
+
|
|
6898
|
+
if (candidates.length === 0) {
|
|
6899
|
+
const placeText = request.place ? ` в ${request.place.locative || request.place.label}` : "";
|
|
6900
|
+
return `В открытом слое не нашел ${entityLabel}${placeText}.`;
|
|
6901
|
+
}
|
|
6902
|
+
|
|
6903
|
+
if (candidates.length > 1 && !request.number) {
|
|
6904
|
+
return [
|
|
6905
|
+
`Нашел несколько подходящих записей${request.place ? ` для ${request.place.locative || request.place.label}` : ""}:`,
|
|
6906
|
+
...candidates.slice(0, 5).flatMap((item) => [
|
|
6907
|
+
`- ${getDirectDataItemName(item)}`,
|
|
6908
|
+
...request.fields.map((field) => ` ${formatDirectDataField(field, item)}`).filter(Boolean),
|
|
6909
|
+
` Источник: слой ${item.layer}, ИНН ${item.inn || "-"}.`,
|
|
6910
|
+
]),
|
|
6911
|
+
].join("\n");
|
|
6912
|
+
}
|
|
6913
|
+
|
|
6914
|
+
const item = candidates[0];
|
|
6915
|
+
const lines = request.fields.map((field) => formatDirectDataField(field, item)).filter(Boolean);
|
|
6916
|
+
if (lines.length === 0) return "";
|
|
6917
|
+
return [
|
|
6918
|
+
...lines,
|
|
6919
|
+
`Источник: слой ${item.layer}, ${getDirectDataItemName(item)}, ИНН ${item.inn || "-"}.`,
|
|
6920
|
+
].join("\n");
|
|
6921
|
+
}
|
|
6922
|
+
|
|
6923
|
+
function itemMatchesPlace(item, place) {
|
|
6924
|
+
if (!place) return true;
|
|
6925
|
+
const text = normalizeEntityText(`${item.name || ""} ${item.address || ""} ${item.legal_address || ""} ${item.fns_full_name || ""} ${item.fns_short_name || ""}`);
|
|
6926
|
+
if (place.id === "yoshkar_ola") return /йошкар|йошкар-ола|йошкар ола/u.test(text) && !/семеновк/u.test(text);
|
|
6927
|
+
return place.aliases.some((alias) => text.includes(normalizeEntityText(alias)));
|
|
6928
|
+
}
|
|
6929
|
+
|
|
6657
6930
|
function pickDirectDataItem(question, dataContext, rows) {
|
|
6658
6931
|
const patterns = dataContext.query?.patterns || extractStructuredPatterns(question);
|
|
6659
6932
|
const targetLayers = patterns.targetLayers || [];
|
|
@@ -8000,6 +8273,14 @@ async function callAiProvider(config, messages) {
|
|
|
8000
8273
|
return callOpenAiCompatible(config, messages, await getApiKey("openrouter"), "OpenRouter");
|
|
8001
8274
|
}
|
|
8002
8275
|
|
|
8276
|
+
if (config.provider === "yandexgpt") {
|
|
8277
|
+
return callYandexGpt(config, messages);
|
|
8278
|
+
}
|
|
8279
|
+
|
|
8280
|
+
if (config.provider === "gigachat") {
|
|
8281
|
+
return callGigaChat(config, messages);
|
|
8282
|
+
}
|
|
8283
|
+
|
|
8003
8284
|
if (config.provider === "codex") {
|
|
8004
8285
|
return callCodex(config, messages);
|
|
8005
8286
|
}
|
|
@@ -8477,6 +8758,96 @@ async function callAiRelayModels(config, apiKey, providerName) {
|
|
|
8477
8758
|
return response.json();
|
|
8478
8759
|
}
|
|
8479
8760
|
|
|
8761
|
+
async function callYandexGpt(config, messages) {
|
|
8762
|
+
const apiKey = await getApiKey("yandexgpt");
|
|
8763
|
+
const folderId = await getYandexFolderId();
|
|
8764
|
+
if (!apiKey || !folderId) {
|
|
8765
|
+
throw new Error("YandexGPT API key или folder ID не найден. Выполните iola ai key set yandexgpt или задайте YANDEXGPT_API_KEY и YANDEXGPT_FOLDER_ID.");
|
|
8766
|
+
}
|
|
8767
|
+
|
|
8768
|
+
const model = config.model || "yandexgpt-lite/latest";
|
|
8769
|
+
const modelUri = model.startsWith("gpt://") ? model : `gpt://${folderId}/${model}`;
|
|
8770
|
+
const response = await fetch(`${String(config.baseUrl || "https://llm.api.cloud.yandex.net/foundationModels/v1").replace(/\/+$/, "")}/completion`, {
|
|
8771
|
+
method: "POST",
|
|
8772
|
+
headers: {
|
|
8773
|
+
authorization: `Api-Key ${apiKey}`,
|
|
8774
|
+
"content-type": "application/json",
|
|
8775
|
+
},
|
|
8776
|
+
body: JSON.stringify({
|
|
8777
|
+
modelUri,
|
|
8778
|
+
completionOptions: {
|
|
8779
|
+
stream: false,
|
|
8780
|
+
temperature: Number(config.temperature ?? 0.2),
|
|
8781
|
+
maxTokens: String(config.maxTokens || 2000),
|
|
8782
|
+
},
|
|
8783
|
+
messages: messages.map((message) => ({
|
|
8784
|
+
role: message.role === "assistant" ? "assistant" : message.role === "system" ? "system" : "user",
|
|
8785
|
+
text: message.content,
|
|
8786
|
+
})),
|
|
8787
|
+
}),
|
|
8788
|
+
});
|
|
8789
|
+
|
|
8790
|
+
if (!response.ok) {
|
|
8791
|
+
const text = await response.text();
|
|
8792
|
+
throw new Error(`YandexGPT request failed: ${response.status} ${response.statusText}\n${sanitizeSecretFromText(text, apiKey)}`);
|
|
8793
|
+
}
|
|
8794
|
+
|
|
8795
|
+
const payload = await response.json();
|
|
8796
|
+
return payload.result?.alternatives?.[0]?.message?.text || "";
|
|
8797
|
+
}
|
|
8798
|
+
|
|
8799
|
+
async function callGigaChat(config, messages) {
|
|
8800
|
+
const authKey = await getApiKey("gigachat");
|
|
8801
|
+
if (!authKey) {
|
|
8802
|
+
throw new Error("GigaChat authorization key не найден. Выполните iola ai key set gigachat или задайте GIGACHAT_AUTH_KEY.");
|
|
8803
|
+
}
|
|
8804
|
+
|
|
8805
|
+
const token = await getGigaChatAccessToken(config, authKey);
|
|
8806
|
+
const response = await fetch(`${String(config.baseUrl || "https://gigachat.devices.sberbank.ru/api/v1").replace(/\/+$/, "")}/chat/completions`, {
|
|
8807
|
+
method: "POST",
|
|
8808
|
+
headers: {
|
|
8809
|
+
authorization: `Bearer ${token}`,
|
|
8810
|
+
"content-type": "application/json",
|
|
8811
|
+
},
|
|
8812
|
+
body: JSON.stringify({
|
|
8813
|
+
model: config.model || "GigaChat-2",
|
|
8814
|
+
messages,
|
|
8815
|
+
temperature: Number(config.temperature ?? 0.2),
|
|
8816
|
+
}),
|
|
8817
|
+
});
|
|
8818
|
+
|
|
8819
|
+
if (!response.ok) {
|
|
8820
|
+
const text = await response.text();
|
|
8821
|
+
throw new Error(`GigaChat request failed: ${response.status} ${response.statusText}\n${sanitizeSecretFromText(text, token)}`);
|
|
8822
|
+
}
|
|
8823
|
+
|
|
8824
|
+
const payload = await response.json();
|
|
8825
|
+
return payload.choices?.[0]?.message?.content || "";
|
|
8826
|
+
}
|
|
8827
|
+
|
|
8828
|
+
async function getGigaChatAccessToken(config, authKey) {
|
|
8829
|
+
const secrets = await loadSecrets();
|
|
8830
|
+
const scope = process.env.GIGACHAT_SCOPE || secrets.gigachat?.scope || config.scope || "GIGACHAT_API_PERS";
|
|
8831
|
+
const response = await fetch(config.authUrl || "https://ngw.devices.sberbank.ru:9443/api/v2/oauth", {
|
|
8832
|
+
method: "POST",
|
|
8833
|
+
headers: {
|
|
8834
|
+
authorization: `Basic ${authKey}`,
|
|
8835
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
8836
|
+
RqUID: randomUUID(),
|
|
8837
|
+
},
|
|
8838
|
+
body: new URLSearchParams({ scope }).toString(),
|
|
8839
|
+
});
|
|
8840
|
+
|
|
8841
|
+
if (!response.ok) {
|
|
8842
|
+
const text = await response.text();
|
|
8843
|
+
throw new Error(`GigaChat token request failed: ${response.status} ${response.statusText}\n${sanitizeSecretFromText(text, authKey)}`);
|
|
8844
|
+
}
|
|
8845
|
+
|
|
8846
|
+
const payload = await response.json();
|
|
8847
|
+
if (!payload.access_token) throw new Error("GigaChat не вернул access_token.");
|
|
8848
|
+
return payload.access_token;
|
|
8849
|
+
}
|
|
8850
|
+
|
|
8480
8851
|
function getAiNetworkMode(config = {}) {
|
|
8481
8852
|
return validateAiNetworkMode(AI_NETWORK_MODE || config.networkMode || "gateway");
|
|
8482
8853
|
}
|
|
@@ -8507,10 +8878,26 @@ async function getApiKey(provider) {
|
|
|
8507
8878
|
return process.env.OPENROUTER_API_KEY;
|
|
8508
8879
|
}
|
|
8509
8880
|
|
|
8881
|
+
if (provider === "yandexgpt" && (process.env.YANDEXGPT_API_KEY || process.env.YANDEX_CLOUD_API_KEY)) {
|
|
8882
|
+
return process.env.YANDEXGPT_API_KEY || process.env.YANDEX_CLOUD_API_KEY;
|
|
8883
|
+
}
|
|
8884
|
+
|
|
8885
|
+
if (provider === "gigachat" && (process.env.GIGACHAT_AUTH_KEY || process.env.GIGACHAT_API_KEY)) {
|
|
8886
|
+
return process.env.GIGACHAT_AUTH_KEY || process.env.GIGACHAT_API_KEY;
|
|
8887
|
+
}
|
|
8888
|
+
|
|
8510
8889
|
const secrets = await loadSecrets();
|
|
8511
8890
|
return secrets[provider]?.apiKey || "";
|
|
8512
8891
|
}
|
|
8513
8892
|
|
|
8893
|
+
async function getYandexFolderId() {
|
|
8894
|
+
if (process.env.YANDEXGPT_FOLDER_ID || process.env.YANDEX_CLOUD_FOLDER_ID) {
|
|
8895
|
+
return process.env.YANDEXGPT_FOLDER_ID || process.env.YANDEX_CLOUD_FOLDER_ID;
|
|
8896
|
+
}
|
|
8897
|
+
const secrets = await loadSecrets();
|
|
8898
|
+
return secrets.yandexgpt?.folderId || "";
|
|
8899
|
+
}
|
|
8900
|
+
|
|
8514
8901
|
async function listLayers(args) {
|
|
8515
8902
|
const options = parseOptions(args);
|
|
8516
8903
|
const info = await fetchJson(`${await getMcpBaseUrl()}/mcp-version`);
|
|
@@ -8768,6 +9155,20 @@ async function onboard(args = []) {
|
|
|
8768
9155
|
await chooseAndSaveApiModel("openrouter");
|
|
8769
9156
|
}
|
|
8770
9157
|
}
|
|
9158
|
+
if (components.includes("yandexgpt")) {
|
|
9159
|
+
await aiSetup(["yandexgpt"]);
|
|
9160
|
+
if (process.stdin.isTTY) {
|
|
9161
|
+
await setAiKey("yandexgpt");
|
|
9162
|
+
await chooseAndSaveApiModel("yandexgpt");
|
|
9163
|
+
}
|
|
9164
|
+
}
|
|
9165
|
+
if (components.includes("gigachat")) {
|
|
9166
|
+
await aiSetup(["gigachat"]);
|
|
9167
|
+
if (process.stdin.isTTY) {
|
|
9168
|
+
await setAiKey("gigachat");
|
|
9169
|
+
await chooseAndSaveApiModel("gigachat");
|
|
9170
|
+
}
|
|
9171
|
+
}
|
|
8771
9172
|
if (components.includes("codex")) {
|
|
8772
9173
|
await installCodexIfMissing();
|
|
8773
9174
|
await aiSetup(["codex"]);
|
|
@@ -8806,14 +9207,16 @@ async function chooseOnboardComponents(status = null) {
|
|
|
8806
9207
|
1: "workspace",
|
|
8807
9208
|
2: "policy",
|
|
8808
9209
|
3: "iola",
|
|
8809
|
-
4: "
|
|
8810
|
-
5: "
|
|
8811
|
-
6: "
|
|
8812
|
-
7: "
|
|
8813
|
-
8: "
|
|
8814
|
-
9: "
|
|
8815
|
-
10: "
|
|
8816
|
-
11: "
|
|
9210
|
+
4: "yandexgpt",
|
|
9211
|
+
5: "gigachat",
|
|
9212
|
+
6: "openai",
|
|
9213
|
+
7: "openrouter",
|
|
9214
|
+
8: "codex",
|
|
9215
|
+
9: "codex-mcp",
|
|
9216
|
+
10: "archive",
|
|
9217
|
+
11: "index",
|
|
9218
|
+
12: "browser",
|
|
9219
|
+
13: "ollama",
|
|
8817
9220
|
};
|
|
8818
9221
|
return [...selected].map((item) => map[item] || item).filter(Boolean);
|
|
8819
9222
|
} finally {
|
|
@@ -8841,6 +9244,8 @@ async function getOnboardComponentStatus() {
|
|
|
8841
9244
|
policy: policyReady,
|
|
8842
9245
|
iola: Boolean(readiness.iola),
|
|
8843
9246
|
ollama: Boolean(ollamaVersion && readiness.ollama),
|
|
9247
|
+
yandexgpt: Boolean(readiness.yandexgpt),
|
|
9248
|
+
gigachat: Boolean(readiness.gigachat),
|
|
8844
9249
|
openai: Boolean(readiness.openai),
|
|
8845
9250
|
openrouter: Boolean(readiness.openrouter),
|
|
8846
9251
|
codex: Boolean(codexVersion !== "не найден" && readiness.codex),
|
|
@@ -8856,14 +9261,16 @@ function onboardComponentRows(status) {
|
|
|
8856
9261
|
["1", "workspace", "workspace и контекст", "рабочая папка, IOLA.md и .iola/context.md"],
|
|
8857
9262
|
["2", "policy", "policy analyst", "разрешения и профиль аналитика"],
|
|
8858
9263
|
["3", "iola", "IOLA локальная модель", "локальная модель найдена"],
|
|
8859
|
-
["4", "
|
|
8860
|
-
["5", "
|
|
8861
|
-
["6", "
|
|
8862
|
-
["7", "
|
|
8863
|
-
["8", "
|
|
8864
|
-
["9", "
|
|
8865
|
-
["10", "
|
|
8866
|
-
["11", "
|
|
9264
|
+
["4", "yandexgpt", "YandexGPT API", "ключ и folder ID сохранены или есть в env"],
|
|
9265
|
+
["5", "gigachat", "GigaChat API", "authorization key сохранен или есть в env"],
|
|
9266
|
+
["6", "openai", "OpenAI API", "API-ключ сохранен или есть в env"],
|
|
9267
|
+
["7", "openrouter", "OpenRouter API", "API-ключ сохранен или есть в env"],
|
|
9268
|
+
["8", "codex", "Codex CLI", "CLI установлен и авторизация найдена"],
|
|
9269
|
+
["9", "codex-mcp", "MCP для Codex", "можно переустановить/обновить"],
|
|
9270
|
+
["10", "archive", "7-Zip / архивы", "архиватор найден"],
|
|
9271
|
+
["11", "index", "Индекс локальных документов", "настраивается под выбранную папку"],
|
|
9272
|
+
["12", "browser", "Browser runtime", "Playwright/Chromium установлен"],
|
|
9273
|
+
["13", "ollama", "Ollama", "опциональный локальный runtime"],
|
|
8867
9274
|
];
|
|
8868
9275
|
return rows.map(([number, key, title, hint]) => ({ number, key, title, hint, status: status[key] ? "готово" : "не настроено" }));
|
|
8869
9276
|
}
|
|
@@ -8873,12 +9280,12 @@ function defaultOnboardSelection(status) {
|
|
|
8873
9280
|
if (!status.workspace) defaults.push("1");
|
|
8874
9281
|
if (!status.policy) defaults.push("2");
|
|
8875
9282
|
if (!status.iola) defaults.push("3");
|
|
8876
|
-
if (!status.archive) defaults.push("
|
|
9283
|
+
if (!status.archive) defaults.push("10");
|
|
8877
9284
|
return defaults.length ? defaults : ["1", "2"];
|
|
8878
9285
|
}
|
|
8879
9286
|
|
|
8880
9287
|
function defaultOnboardComponents(status) {
|
|
8881
|
-
const map = { 1: "workspace", 2: "policy", 3: "iola", 4: "
|
|
9288
|
+
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" };
|
|
8882
9289
|
return defaultOnboardSelection(status).map((item) => map[item]).filter(Boolean);
|
|
8883
9290
|
}
|
|
8884
9291
|
|
|
@@ -10854,6 +11261,9 @@ function sanitizeConfig(config) {
|
|
|
10854
11261
|
if (profile?.provider === "openai" || profile?.provider === "openrouter") {
|
|
10855
11262
|
profile.networkMode = profile.networkMode || "gateway";
|
|
10856
11263
|
}
|
|
11264
|
+
if (profile?.provider === "yandexgpt" || profile?.provider === "gigachat") {
|
|
11265
|
+
profile.networkMode = profile.networkMode || "direct";
|
|
11266
|
+
}
|
|
10857
11267
|
}
|
|
10858
11268
|
return next;
|
|
10859
11269
|
}
|
|
@@ -10866,7 +11276,7 @@ function validateConfig(config) {
|
|
|
10866
11276
|
if (!config.ai?.profiles || typeof config.ai.profiles !== "object") errors.push("ai.profiles обязателен");
|
|
10867
11277
|
if (config.ai?.activeProfile && !config.ai.profiles?.[config.ai.activeProfile]) errors.push(`ai.activeProfile не найден в profiles: ${config.ai.activeProfile}`);
|
|
10868
11278
|
for (const [name, profile] of Object.entries(config.ai?.profiles || {})) {
|
|
10869
|
-
if (!["iola", "ollama", "openai", "openrouter", "codex"].includes(profile.provider)) errors.push(`ai.profiles.${name}.provider неизвестен`);
|
|
11279
|
+
if (!["iola", "ollama", "yandexgpt", "gigachat", "openai", "openrouter", "codex"].includes(profile.provider)) errors.push(`ai.profiles.${name}.provider неизвестен`);
|
|
10870
11280
|
if (profile.provider !== "codex" && profile.provider !== "iola" && !profile.baseUrl) errors.push(`ai.profiles.${name}.baseUrl обязателен`);
|
|
10871
11281
|
if (profile.networkMode && !["direct", "gateway", "auto"].includes(profile.networkMode)) errors.push(`ai.profiles.${name}.networkMode должен быть direct, gateway или auto`);
|
|
10872
11282
|
}
|
|
@@ -10885,7 +11295,7 @@ function configSchema() {
|
|
|
10885
11295
|
required: ["api", "ai"],
|
|
10886
11296
|
properties: {
|
|
10887
11297
|
api: { required: ["baseUrl", "mcpBaseUrl"] },
|
|
10888
|
-
ai: { required: ["activeProfile", "profiles"], providers: ["iola", "ollama", "openai", "openrouter", "codex"] },
|
|
11298
|
+
ai: { required: ["activeProfile", "profiles"], providers: ["iola", "ollama", "yandexgpt", "gigachat", "openai", "openrouter", "codex"] },
|
|
10889
11299
|
permissions: { localTools: ALL_LOCAL_TOOLS, runtime: ["readFiles", "writeFiles", "editFiles", "deleteFiles", "sync", "externalApi", "externalAi", "codex"] },
|
|
10890
11300
|
toolsets: { available: Object.keys(TOOLSETS) },
|
|
10891
11301
|
files: { modes: ["locked", "read-only", "workspace-write", "full-access"], approvals: ["never", "on-write", "on-danger", "always"] },
|
package/test/smoke-test.js
CHANGED
|
@@ -84,4 +84,18 @@ if (deletePlan.willKeep.includes("npm package files")) {
|
|
|
84
84
|
throw new Error("delete dry-run should not keep npm package files");
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
const multiSchoolAnswer = await runCli(["ask", "Кто директор школы № 2 и адрес школы № 7?", "--profile", "yandexgpt", "--no-history"]);
|
|
88
|
+
assertIncludes(multiSchoolAnswer, "Адамова Наталья Васильевна", "multi school answer");
|
|
89
|
+
assertIncludes(multiSchoolAnswer, "улица Первомайская, дом 89", "multi school answer");
|
|
90
|
+
assertNotIncludes(multiSchoolAnswer, "улица Осипенко, дом 46\nИсточник: слой schools, МБОУ \"Средняя общеобразовательная школа № 7", "multi school answer");
|
|
91
|
+
|
|
92
|
+
const externalTownAnswer = await runCli(["ask", "Адрес школы № 1 Козьмодемьянска", "--profile", "yandexgpt", "--no-history"]);
|
|
93
|
+
assertIncludes(externalTownAnswer, "Данных по Козьмодемьянске", "external town answer");
|
|
94
|
+
assertNotIncludes(externalTownAnswer, "улица Петрова, дом 15", "external town answer");
|
|
95
|
+
|
|
96
|
+
const semenovkaAnswer = await runCli(["ask", "Адрес школы № 1 Семеновки", "--profile", "yandexgpt", "--no-history"]);
|
|
97
|
+
assertIncludes(semenovkaAnswer, "Точную школу № 1 в Семёновке", "semenovka answer");
|
|
98
|
+
assertIncludes(semenovkaAnswer, "село Семёновка", "semenovka answer");
|
|
99
|
+
assertNotIncludes(semenovkaAnswer, "улица Петрова, дом 15", "semenovka answer");
|
|
100
|
+
|
|
87
101
|
console.log("smoke tests passed");
|
|
@@ -18,22 +18,181 @@ iola ask "найди школы на Петрова"
|
|
|
18
18
|
|
|
19
19
|
Если выбранная Ollama-модель еще не установлена, CLI предложит скачать ее через `ollama pull`. При выборе сторонней Ollama-модели профиль `local` сохраняется как `provider: ollama`; при выборе штатной модели IOLA профиль `local` возвращается к `provider: iola`.
|
|
20
20
|
|
|
21
|
+
## Российские AI
|
|
22
|
+
|
|
23
|
+
Российские провайдеры вынесены в отдельный блок `/model` и вызываются напрямую, без gateway/proxy:
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
/model
|
|
27
|
+
2. Российские AI (YandexGPT/GigaChat)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### YandexGPT
|
|
31
|
+
|
|
32
|
+
Для CLI нужны две вещи: API key и ID каталога Yandex Cloud.
|
|
33
|
+
|
|
34
|
+
Пошаговая настройка:
|
|
35
|
+
|
|
36
|
+
1. Откройте консоль Yandex Cloud: `https://console.yandex.cloud/`.
|
|
37
|
+
2. Войдите в аккаунт Яндекса.
|
|
38
|
+
3. Если Yandex Cloud попросит создать облако, создайте облако. Название может быть любым, например `iola-cli`.
|
|
39
|
+
4. Откройте каталог внутри облака. Если каталога нет, создайте каталог. Название может быть любым, например `default`.
|
|
40
|
+
5. На верхней панели страницы каталога найдите название каталога и его идентификатор. В нашем примере рядом с `default` был показан ID вида `b1g8...`.
|
|
41
|
+
6. Нажмите кнопку `Копировать` рядом с идентификатором каталога. Это значение будет `YANDEXGPT_FOLDER_ID`.
|
|
42
|
+
7. В левом меню или через поиск сервисов откройте `Identity and Access Management`.
|
|
43
|
+
8. Внутри `Identity and Access Management` откройте раздел `Сервисные аккаунты`.
|
|
44
|
+
9. Нажмите `Создать сервисный аккаунт`.
|
|
45
|
+
10. В поле `Имя` введите понятное имя, например `iola-cli`.
|
|
46
|
+
11. В поле `Описание` можно написать `API-доступ iola-cli к YandexGPT`.
|
|
47
|
+
12. В блоке `Роли в каталоге` нажмите `Добавить роль`.
|
|
48
|
+
13. В поле поиска роли введите `ai.languageModels.user`.
|
|
49
|
+
14. В найденной группе `ai.languageModels` выберите роль `user`. В форме должна появиться роль `ai.languageModels.user`.
|
|
50
|
+
15. Нажмите `Создать`.
|
|
51
|
+
16. После создания откройте созданный сервисный аккаунт `iola-cli`.
|
|
52
|
+
17. На странице сервисного аккаунта нажмите кнопку `Создать новый ключ`.
|
|
53
|
+
18. В выпадающем меню выберите `Создать API-ключ`. Не выбирайте `Создать статический ключ доступа` и не выбирайте `Создать авторизованный ключ`.
|
|
54
|
+
19. В поле `Описание` напишите, например, `iola-cli YandexGPT API key`.
|
|
55
|
+
20. В поле `Область действия` выберите `yc.ai.foundationModels.execute`. Если список пустой, начните вводить это значение в поле фильтра и выберите найденный вариант.
|
|
56
|
+
21. Поле `Срок действия` можно оставить пустым или выбрать дату окончания действия ключа по вашей политике безопасности.
|
|
57
|
+
22. Нажмите `Создать`.
|
|
58
|
+
23. Yandex Cloud покажет `Идентификатор ключа` и `Ваш секретный ключ`. Секретный ключ показывается только один раз.
|
|
59
|
+
24. Скопируйте `Ваш секретный ключ` и сохраните его локально. Это значение будет `YANDEXGPT_API_KEY`.
|
|
60
|
+
25. Сохраните ключ в CLI:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
iola ai key set yandexgpt
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
CLI попросит ввести:
|
|
67
|
+
|
|
68
|
+
- `YANDEXGPT_API_KEY` - API-ключ сервисного аккаунта;
|
|
69
|
+
- `YANDEXGPT_FOLDER_ID` - ID каталога Yandex Cloud.
|
|
70
|
+
|
|
71
|
+
Важно: `Идентификатор ключа` - это не API-ключ для CLI. Для CLI нужен именно `Ваш секретный ключ`. Если закрыть окно создания ключа, секретный ключ больше нельзя посмотреть. В этом случае удалите старый API-ключ и создайте новый.
|
|
72
|
+
|
|
73
|
+
После сохранения ключа выберите профиль и модель:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
iola ai setup yandexgpt --model yandexgpt-lite/latest
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Через интерактивное меню:
|
|
80
|
+
|
|
81
|
+
```text
|
|
82
|
+
/model
|
|
83
|
+
2. Российские AI (YandexGPT/GigaChat)
|
|
84
|
+
1. YandexGPT API
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Доступные модели в CLI:
|
|
88
|
+
|
|
89
|
+
- `yandexgpt-lite/latest` - быстрый и более дешевый вариант;
|
|
90
|
+
- `yandexgpt/latest` - YandexGPT Pro latest;
|
|
91
|
+
- `yandexgpt/rc` - release candidate.
|
|
92
|
+
|
|
93
|
+
CLI также понимает env-переменные `YANDEXGPT_API_KEY` или `YANDEX_CLOUD_API_KEY`, а для каталога - `YANDEXGPT_FOLDER_ID` или `YANDEX_CLOUD_FOLDER_ID`.
|
|
94
|
+
|
|
95
|
+
### GigaChat
|
|
96
|
+
|
|
97
|
+
Для CLI нужен authorization key. Это не одноразовый OAuth access token: CLI сам получает OAuth-токен перед запросом, используя сохраненный authorization key.
|
|
98
|
+
|
|
99
|
+
Пошаговая настройка:
|
|
100
|
+
|
|
101
|
+
1. Перейдите на портал разработчиков Сбера: `https://developers.sber.ru/`.
|
|
102
|
+
2. Войдите в аккаунт.
|
|
103
|
+
3. Откройте раздел GigaChat.
|
|
104
|
+
4. Если нужно, подключите доступ к GigaChat API для физического лица или организации.
|
|
105
|
+
5. Откройте кабинет/проект, в котором доступны учетные данные GigaChat API.
|
|
106
|
+
6. Найдите authorization key для REST API. В документации GigaChat он используется в OAuth-запросе в заголовке `Authorization: Basic ...`.
|
|
107
|
+
7. Скопируйте authorization key и сохраните его локально в CLI:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
iola ai key set gigachat
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
CLI попросит ввести:
|
|
114
|
+
|
|
115
|
+
- `GIGACHAT_AUTH_KEY` - authorization key;
|
|
116
|
+
- `scope` - по умолчанию `GIGACHAT_API_PERS` для персонального доступа.
|
|
117
|
+
|
|
118
|
+
После сохранения ключа выберите профиль и модель:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
iola ai setup gigachat --model GigaChat-2
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Через интерактивное меню:
|
|
125
|
+
|
|
126
|
+
```text
|
|
127
|
+
/model
|
|
128
|
+
2. Российские AI (YandexGPT/GigaChat)
|
|
129
|
+
2. GigaChat API
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Доступные модели в CLI:
|
|
133
|
+
|
|
134
|
+
- `GigaChat-2` - основная модель;
|
|
135
|
+
- `GigaChat-2-Pro` - повышенное качество;
|
|
136
|
+
- `GigaChat-2-Max` - максимальное качество;
|
|
137
|
+
- `GigaChat` - legacy/fallback.
|
|
138
|
+
|
|
139
|
+
CLI также понимает env-переменные `GIGACHAT_AUTH_KEY` или `GIGACHAT_API_KEY`. По умолчанию используется scope `GIGACHAT_API_PERS`; при необходимости его можно задать через `GIGACHAT_SCOPE`.
|
|
140
|
+
|
|
141
|
+
По тарифам: у GigaChat для физических лиц есть Freemium-лимит на токены; для больших объемов используются платные пакеты. У YandexGPT тарификация идет через Yandex Cloud по токенам и квотам аккаунта, поэтому актуальные бесплатные гранты или лимиты нужно проверять в консоли Yandex Cloud.
|
|
142
|
+
|
|
143
|
+
Официальная документация:
|
|
144
|
+
|
|
145
|
+
- Yandex Foundation Models: `https://yandex.cloud/ru/docs/foundation-models/`;
|
|
146
|
+
- Yandex AI Studio authentication: `https://yandex.cloud/ru/docs/ai-studio/api-ref/authentication`;
|
|
147
|
+
- Yandex Foundation Models pricing: `https://yandex.cloud/ru/docs/foundation-models/pricing`;
|
|
148
|
+
- GigaChat overview: `https://developers.sber.ru/docs/ru/gigachat/overview`;
|
|
149
|
+
- GigaChat OAuth token: `https://developers.sber.ru/docs/ru/gigachat/api/reference/rest/post-token`;
|
|
150
|
+
- GigaChat tariffs: `https://developers.sber.ru/docs/ru/gigachat/tariffs`.
|
|
151
|
+
|
|
21
152
|
## OpenAI
|
|
22
153
|
|
|
154
|
+
Получение ключа OpenAI Platform:
|
|
155
|
+
|
|
156
|
+
1. Зарегистрируйтесь или войдите в OpenAI Platform: `https://platform.openai.com/`.
|
|
157
|
+
2. Откройте страницу API-ключей: `https://platform.openai.com/api-keys`.
|
|
158
|
+
3. Выберите нужный project или создайте новый project.
|
|
159
|
+
4. Нажмите `Create new secret key`.
|
|
160
|
+
5. Скопируйте ключ сразу после создания. Повторно посмотреть полный ключ обычно нельзя.
|
|
161
|
+
6. В CLI сохраните ключ:
|
|
162
|
+
|
|
23
163
|
```bash
|
|
24
164
|
iola ai key set openai
|
|
25
165
|
iola ai setup openai --model gpt-4.1-mini
|
|
26
166
|
iola ask "найди школу 29" --profile openai
|
|
27
167
|
```
|
|
28
168
|
|
|
169
|
+
Важно: OpenAI API Platform и ChatGPT Plus/Pro - разные продукты. Подписка ChatGPT не заменяет API-биллинг. Для работы API обычно нужно отдельно настроить billing в OpenAI Platform.
|
|
170
|
+
|
|
29
171
|
## OpenRouter
|
|
30
172
|
|
|
173
|
+
Получение ключа OpenRouter:
|
|
174
|
+
|
|
175
|
+
1. Зарегистрируйтесь или войдите в OpenRouter: `https://openrouter.ai/`.
|
|
176
|
+
2. Откройте страницу ключей: `https://openrouter.ai/settings/keys`.
|
|
177
|
+
3. Нажмите создание нового API key.
|
|
178
|
+
4. Задайте имя ключа и при необходимости лимит расходов.
|
|
179
|
+
5. Скопируйте ключ сразу после создания. Храните его как секрет.
|
|
180
|
+
6. В CLI сохраните ключ:
|
|
181
|
+
|
|
31
182
|
```bash
|
|
32
183
|
iola ai key set openrouter
|
|
33
184
|
iola ai setup openrouter --model openai/gpt-4.1-mini
|
|
34
185
|
iola ai models openrouter --search qwen
|
|
35
186
|
```
|
|
36
187
|
|
|
188
|
+
OpenRouter удобен тем, что через один ключ можно выбирать модели разных разработчиков: OpenAI, Anthropic, Google, Qwen / Alibaba, DeepSeek, Meta / Llama, Mistral AI и других.
|
|
189
|
+
|
|
190
|
+
## Оплата
|
|
191
|
+
|
|
192
|
+
Важно: оплата российскими банковскими картами для OpenAI Platform и OpenRouter может быть невозможна. Перед настройкой платных API заранее проверьте доступный способ оплаты и пополнения баланса в личном кабинете выбранного сервиса.
|
|
193
|
+
|
|
194
|
+
Ключи YandexGPT, GigaChat, OpenAI и OpenRouter сохраняются локально на устройстве пользователя в `~/.iola/secrets.json`. CLI не публикует ключи в репозиторий и не записывает их в документацию.
|
|
195
|
+
|
|
37
196
|
В интерактивном CLI модели удобнее выбирать через slash-команду:
|
|
38
197
|
|
|
39
198
|
```text
|
|
@@ -64,4 +223,4 @@ iola ai profile use local
|
|
|
64
223
|
iola ai profile use openrouter
|
|
65
224
|
```
|
|
66
225
|
|
|
67
|
-
В интерактивном агенте можно использовать `/model`, чтобы выбрать подключение и модель без ручного ввода id модели.
|
|
226
|
+
В интерактивном агенте можно использовать `/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
|
Быстрый старт:
|
|
@@ -30,33 +30,45 @@ iola master
|
|
|
30
30
|
|
|
31
31
|
После настройки локальную модель можно менять в интерактивном агенте через `/model`. Помимо штатной IOLA-модели можно выбрать установленную или рекомендуемую Ollama-модель, либо вручную ввести имя любой модели из библиотеки Ollama.
|
|
32
32
|
|
|
33
|
-
### 4.
|
|
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
|
|
34
46
|
|
|
35
47
|
Настраивает профиль OpenAI и сохраняет API-ключ локально у пользователя.
|
|
36
48
|
|
|
37
49
|
После сохранения ключа мастер предлагает выбрать модель из доступного списка.
|
|
38
50
|
|
|
39
|
-
###
|
|
51
|
+
### 7. OpenRouter API
|
|
40
52
|
|
|
41
53
|
Настраивает профиль OpenRouter и сохраняет API-ключ локально у пользователя.
|
|
42
54
|
|
|
43
55
|
После сохранения ключа мастер предлагает выбрать разработчика моделей OpenRouter, а затем одну из свежих моделей для текстовой работы. В списке моделей `0` возвращает к выбору разработчика.
|
|
44
56
|
|
|
45
|
-
###
|
|
57
|
+
### 8. Codex CLI
|
|
46
58
|
|
|
47
59
|
Проверяет наличие Codex CLI и авторизации. Если Codex уже установлен и вход выполнен, пункт показывается как `готово`.
|
|
48
60
|
|
|
49
|
-
###
|
|
61
|
+
### 9. MCP для Codex
|
|
50
62
|
|
|
51
63
|
Добавляет MCP-сервер открытых данных Йошкар-Олы в Codex. Этот пункт можно запускать повторно для обновления подключения.
|
|
52
64
|
|
|
53
|
-
###
|
|
65
|
+
### 10. 7-Zip / архивы
|
|
54
66
|
|
|
55
67
|
Проверяет 7-Zip. Если архиватор не найден, мастер устанавливает его.
|
|
56
68
|
|
|
57
69
|
Нужен для чтения, распаковки, сборки и индексирования архивов.
|
|
58
70
|
|
|
59
|
-
###
|
|
71
|
+
### 11. Индекс локальных документов
|
|
60
72
|
|
|
61
73
|
Готовит режим чтения файлов и подсказывает команду индексирования локальной папки:
|
|
62
74
|
|
|
@@ -64,10 +76,14 @@ iola master
|
|
|
64
76
|
iola index folder ./docs
|
|
65
77
|
```
|
|
66
78
|
|
|
67
|
-
###
|
|
79
|
+
### 12. Browser runtime
|
|
68
80
|
|
|
69
81
|
Проверяет Playwright/Chromium runtime. Нужен для браузерного агента.
|
|
70
82
|
|
|
83
|
+
### 13. Ollama
|
|
84
|
+
|
|
85
|
+
Опциональный локальный runtime для выбора сторонних моделей из библиотеки Ollama.
|
|
86
|
+
|
|
71
87
|
Если runtime уже установлен, пункт показывается как `готово`.
|
|
72
88
|
|
|
73
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
|