@iola_adm/iola-cli 0.1.104 → 0.1.106
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/package.json +1 -1
- package/src/cli.js +183 -3
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -15,6 +15,8 @@ import { inflateRawSync, inflateSync } from "node:zlib";
|
|
|
15
15
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
16
|
const API_BASE_URL = process.env.IOLA_API_BASE_URL || "https://apiiola.yasg.ru/api/v1";
|
|
17
17
|
const MCP_BASE_URL = process.env.IOLA_MCP_BASE_URL || "https://apiiola.yasg.ru";
|
|
18
|
+
const AI_RELAY_BASE_URL = process.env.IOLA_AI_RELAY_BASE_URL || `${API_BASE_URL}/ai/relay`;
|
|
19
|
+
const AI_NETWORK_MODE = process.env.IOLA_AI_NETWORK_MODE || "";
|
|
18
20
|
const MIN_NODE_VERSION = "22.5.0";
|
|
19
21
|
const CONFIG_DIR = path.join(os.homedir(), ".iola");
|
|
20
22
|
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
@@ -121,6 +123,7 @@ const DEFAULT_AI_CONFIG = {
|
|
|
121
123
|
api: {
|
|
122
124
|
baseUrl: "https://apiiola.yasg.ru/api/v1",
|
|
123
125
|
mcpBaseUrl: "https://apiiola.yasg.ru",
|
|
126
|
+
aiRelayBaseUrl: "https://apiiola.yasg.ru/api/v1/ai/relay",
|
|
124
127
|
},
|
|
125
128
|
ai: {
|
|
126
129
|
activeProfile: "local",
|
|
@@ -140,11 +143,13 @@ const DEFAULT_AI_CONFIG = {
|
|
|
140
143
|
provider: "openai",
|
|
141
144
|
model: "gpt-4.1-mini",
|
|
142
145
|
baseUrl: "https://api.openai.com/v1",
|
|
146
|
+
networkMode: "gateway",
|
|
143
147
|
},
|
|
144
148
|
openrouter: {
|
|
145
149
|
provider: "openrouter",
|
|
146
150
|
model: "openai/gpt-4.1-mini",
|
|
147
151
|
baseUrl: "https://openrouter.ai/api/v1",
|
|
152
|
+
networkMode: "gateway",
|
|
148
153
|
},
|
|
149
154
|
codex: {
|
|
150
155
|
provider: "codex",
|
|
@@ -625,6 +630,8 @@ Usage:
|
|
|
625
630
|
Environment:
|
|
626
631
|
IOLA_API_BASE_URL default: ${API_BASE_URL}
|
|
627
632
|
IOLA_MCP_BASE_URL default: ${MCP_BASE_URL}
|
|
633
|
+
IOLA_AI_RELAY_BASE_URL default: ${AI_RELAY_BASE_URL}
|
|
634
|
+
IOLA_AI_NETWORK_MODE direct|gateway|auto
|
|
628
635
|
|
|
629
636
|
Requirements:
|
|
630
637
|
Node.js >= ${MIN_NODE_VERSION}
|
|
@@ -4327,6 +4334,13 @@ async function listAiModels(provider) {
|
|
|
4327
4334
|
if (!apiKey) {
|
|
4328
4335
|
throw new Error("OpenAI API key не найден. Выполните iola ai key set openai.");
|
|
4329
4336
|
}
|
|
4337
|
+
const relayConfig = await getApiProviderNetworkConfig("openai");
|
|
4338
|
+
if (getAiNetworkMode(relayConfig) === "gateway") {
|
|
4339
|
+
const payload = await callAiRelayModels(relayConfig, apiKey, "OpenAI");
|
|
4340
|
+
return (payload.data || [])
|
|
4341
|
+
.map((model) => ({ id: model.id, provider: "openai", note: model.owned_by || "" }))
|
|
4342
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
4343
|
+
}
|
|
4330
4344
|
const response = await fetch("https://api.openai.com/v1/models", {
|
|
4331
4345
|
headers: { authorization: `Bearer ${apiKey}` },
|
|
4332
4346
|
});
|
|
@@ -4342,6 +4356,18 @@ async function listAiModels(provider) {
|
|
|
4342
4356
|
}
|
|
4343
4357
|
|
|
4344
4358
|
if (provider === "openrouter") {
|
|
4359
|
+
const apiKey = await getApiKey("openrouter");
|
|
4360
|
+
const relayConfig = await getApiProviderNetworkConfig("openrouter");
|
|
4361
|
+
if (apiKey && getAiNetworkMode(relayConfig) === "gateway") {
|
|
4362
|
+
const payload = await callAiRelayModels(relayConfig, apiKey, "OpenRouter");
|
|
4363
|
+
return (payload.data || [])
|
|
4364
|
+
.map((model) => ({
|
|
4365
|
+
id: model.id,
|
|
4366
|
+
provider: "openrouter",
|
|
4367
|
+
note: model.name || "",
|
|
4368
|
+
}))
|
|
4369
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
4370
|
+
}
|
|
4345
4371
|
const response = await fetch("https://openrouter.ai/api/v1/models", {
|
|
4346
4372
|
headers: { accept: "application/json" },
|
|
4347
4373
|
});
|
|
@@ -4369,6 +4395,16 @@ async function listAiModels(provider) {
|
|
|
4369
4395
|
];
|
|
4370
4396
|
}
|
|
4371
4397
|
|
|
4398
|
+
async function getApiProviderNetworkConfig(provider) {
|
|
4399
|
+
const config = await loadConfig();
|
|
4400
|
+
const profile = config.ai.profiles?.[provider] || DEFAULT_AI_CONFIG.ai.profiles[provider] || {};
|
|
4401
|
+
return {
|
|
4402
|
+
provider,
|
|
4403
|
+
...profile,
|
|
4404
|
+
aiRelayBaseUrl: profile.aiRelayBaseUrl || config.api?.aiRelayBaseUrl || AI_RELAY_BASE_URL,
|
|
4405
|
+
};
|
|
4406
|
+
}
|
|
4407
|
+
|
|
4372
4408
|
function getRecommendedOllamaModels(notePrefix = "recommended") {
|
|
4373
4409
|
return [
|
|
4374
4410
|
{ id: IOLA_LOCAL_OLLAMA_MODEL, provider: "ollama", note: `${notePrefix} IOLA default low RAM` },
|
|
@@ -4389,7 +4425,9 @@ async function printAiProfiles() {
|
|
|
4389
4425
|
provider: profile.provider,
|
|
4390
4426
|
model: profile.model || "-",
|
|
4391
4427
|
baseUrl: profile.baseUrl || "-",
|
|
4392
|
-
mode: profile.provider === "codex"
|
|
4428
|
+
mode: profile.provider === "codex"
|
|
4429
|
+
? `sandbox=${profile.sandbox || "read-only"}, approval=${profile.approval || "never"}`
|
|
4430
|
+
: (profile.provider === "openai" || profile.provider === "openrouter" ? `network=${getAiNetworkMode(profile)}` : "-"),
|
|
4393
4431
|
}));
|
|
4394
4432
|
|
|
4395
4433
|
printTable(rows, [
|
|
@@ -4512,6 +4550,12 @@ function buildProfileFromOptions(provider, options) {
|
|
|
4512
4550
|
if (options["base-url"]) {
|
|
4513
4551
|
profile.baseUrl = options["base-url"];
|
|
4514
4552
|
}
|
|
4553
|
+
if (options["network-mode"]) {
|
|
4554
|
+
profile.networkMode = validateAiNetworkMode(options["network-mode"]);
|
|
4555
|
+
}
|
|
4556
|
+
if (options["relay-url"]) {
|
|
4557
|
+
profile.aiRelayBaseUrl = options["relay-url"];
|
|
4558
|
+
}
|
|
4515
4559
|
|
|
4516
4560
|
if (provider === "iola") {
|
|
4517
4561
|
profile.runtime = options.runtime || defaults.runtime || "ollama";
|
|
@@ -6473,6 +6517,8 @@ function resolveAiProfile(config, options = {}) {
|
|
|
6473
6517
|
provider,
|
|
6474
6518
|
model: options.model || activeProfile.model || config.ai.model,
|
|
6475
6519
|
baseUrl: options["base-url"] || activeProfile.baseUrl || config.ai.baseUrl,
|
|
6520
|
+
aiRelayBaseUrl: options["relay-url"] || activeProfile.aiRelayBaseUrl || config.api?.aiRelayBaseUrl || AI_RELAY_BASE_URL,
|
|
6521
|
+
networkMode: options["network-mode"] || activeProfile.networkMode,
|
|
6476
6522
|
repo: options.repo || activeProfile.repo,
|
|
6477
6523
|
modelDir: options["model-dir"] || activeProfile.modelDir,
|
|
6478
6524
|
temperature: options.temperature || activeProfile.temperature,
|
|
@@ -6551,12 +6597,33 @@ async function localToolAsk(question, providerConfig, options) {
|
|
|
6551
6597
|
|
|
6552
6598
|
function guardNonPublicQuestion(question) {
|
|
6553
6599
|
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
6600
|
+
if (isDangerousInstructionQuestion(normalized)) {
|
|
6601
|
+
return "Не могу помогать с созданием оружия, взрывчатых веществ или инструкциями по причинению вреда.";
|
|
6602
|
+
}
|
|
6554
6603
|
if (/(зарплат|получа[ею]т|доход|домашн|паспорт|снилс|личн|персональн)/iu.test(normalized)) {
|
|
6555
6604
|
return "Это поле не входит в открытые публичные данные.";
|
|
6556
6605
|
}
|
|
6606
|
+
if (isUnsupportedPublicEntityQuestion(normalized)) {
|
|
6607
|
+
return "Сейчас в открытых слоях CLI подключены школы и детские сады. По музеям, маршрутам, больницам и другим организациям я пока не могу давать проверяемые ответы.";
|
|
6608
|
+
}
|
|
6557
6609
|
return "";
|
|
6558
6610
|
}
|
|
6559
6611
|
|
|
6612
|
+
function isDangerousInstructionQuestion(normalized) {
|
|
6613
|
+
const asksHow = /(как|сдела|собра|изготов|созда|надо|нужн|инструкц|рецепт|схем|компонент)/iu.test(normalized);
|
|
6614
|
+
const weapon = /(бомб|взрывчат|взрывн|детонатор|термит|напалм|оруж|патрон|яд|отрав|боеприпас|мина|гранат)/iu.test(normalized);
|
|
6615
|
+
const nuclear = /(атомн|ядерн|уран|плутони|обогащен)/iu.test(normalized) && /(бомб|оруж|собра|сдела|созда|надо|нужн)/iu.test(normalized);
|
|
6616
|
+
return (asksHow && weapon) || nuclear;
|
|
6617
|
+
}
|
|
6618
|
+
|
|
6619
|
+
function isUnsupportedPublicEntityQuestion(normalized) {
|
|
6620
|
+
const unsupportedEntity = /(музе[йяею]|театр|больниц|поликлиник|аптек|магазин|кафе|ресторан|гостиниц|банк|мфц|библиотек|парк|маршрут|остановк)/iu.test(normalized);
|
|
6621
|
+
if (!unsupportedEntity) return false;
|
|
6622
|
+
const supportedEducation = /(школ|гимнази|лице|детск\w*\s+сад|детсад|садик)/iu.test(normalized);
|
|
6623
|
+
if (supportedEducation) return false;
|
|
6624
|
+
return /(адрес|где|как пройти|как добраться|директ|руководител|заведующ|телефон|сайт|почт|инн|кто|находится)/iu.test(normalized);
|
|
6625
|
+
}
|
|
6626
|
+
|
|
6560
6627
|
function buildCasualDirectAnswer(question) {
|
|
6561
6628
|
const normalized = String(question || "").toLocaleLowerCase("ru-RU").trim();
|
|
6562
6629
|
if (/^(кто ты|что ты|какая ты модель|что ты за модель|что за модель|какая модель|назови модель|ты какая модель|ты кто)([?.!\s]*)$/iu.test(normalized)) {
|
|
@@ -6767,8 +6834,10 @@ function validateToolPlan(plan, options = {}) {
|
|
|
6767
6834
|
}
|
|
6768
6835
|
|
|
6769
6836
|
async function searchPublicEntities(args = {}) {
|
|
6837
|
+
const layer = normalizeEntityLayer(args.layer);
|
|
6838
|
+
assertSupportedPublicEntityLayer(layer);
|
|
6770
6839
|
const payload = await postJson(`${await getApiBaseUrl()}/search-entities`, {
|
|
6771
|
-
layer
|
|
6840
|
+
layer,
|
|
6772
6841
|
query: args.query || args.entity_name || args.name || "",
|
|
6773
6842
|
limit: Number(args.limit || 10),
|
|
6774
6843
|
filters: args.filters || undefined,
|
|
@@ -6784,6 +6853,7 @@ async function resolvePublicEntityField(args = {}) {
|
|
|
6784
6853
|
const endpoint = `${await getApiBaseUrl()}/resolve-entity-field`;
|
|
6785
6854
|
const requestedField = normalizeEntityField(args.field);
|
|
6786
6855
|
const layer = normalizeEntityLayer(args.layer);
|
|
6856
|
+
assertSupportedPublicEntityLayer(layer);
|
|
6787
6857
|
const strictQuestionNumber = extractEntityNumberFromQuestion(args.source_question, layer);
|
|
6788
6858
|
const payload = {
|
|
6789
6859
|
layer,
|
|
@@ -6988,9 +7058,24 @@ function normalizeEntityLayer(layer) {
|
|
|
6988
7058
|
const value = String(layer || "").toLocaleLowerCase("ru-RU");
|
|
6989
7059
|
if (value === "school" || value === "schools" || value.includes("школ")) return "schools";
|
|
6990
7060
|
if (value === "kindergarten" || value === "kindergartens" || value.includes("сад")) return "kindergartens";
|
|
7061
|
+
if (value === "museum" || value === "museums" || value.includes("музе")) return "museums";
|
|
6991
7062
|
return value || "schools";
|
|
6992
7063
|
}
|
|
6993
7064
|
|
|
7065
|
+
function assertSupportedPublicEntityLayer(layer) {
|
|
7066
|
+
if (layer === "schools" || layer === "kindergartens") return;
|
|
7067
|
+
throw createUnsupportedPublicDatasetError(layer);
|
|
7068
|
+
}
|
|
7069
|
+
|
|
7070
|
+
function createUnsupportedPublicDatasetError(layer) {
|
|
7071
|
+
const detail = {
|
|
7072
|
+
error: "unsupported_public_dataset",
|
|
7073
|
+
message: "Unknown public dataset",
|
|
7074
|
+
layer,
|
|
7075
|
+
};
|
|
7076
|
+
return new Error(`Request failed: 400 Bad Request (local-validation)\n${JSON.stringify({ detail })}`);
|
|
7077
|
+
}
|
|
7078
|
+
|
|
6994
7079
|
function normalizeEntityField(field) {
|
|
6995
7080
|
const value = String(field || "").toLocaleLowerCase("ru-RU");
|
|
6996
7081
|
if (value === "director" || value === "directors" || value === "directs" || value === "direct" || value === "head" || value === "head_name" || value.includes("директ") || value.includes("руковод")) return "director";
|
|
@@ -7037,6 +7122,9 @@ function isLocalEntityValidationError(error) {
|
|
|
7037
7122
|
|
|
7038
7123
|
function formatToolExecutionError(error, plan) {
|
|
7039
7124
|
const details = parseErrorJsonDetails(error);
|
|
7125
|
+
if (details?.error === "unsupported_public_dataset" || details === "Unknown public dataset") {
|
|
7126
|
+
return "Сейчас в открытых слоях CLI подключены школы и детские сады. По этому типу организаций я пока не могу давать проверяемые ответы.";
|
|
7127
|
+
}
|
|
7040
7128
|
if (details?.error !== "entity_not_found") return "";
|
|
7041
7129
|
if (details.local_validation && details.entity_name) {
|
|
7042
7130
|
return `В открытом слое не нашел организацию по названию "${details.entity_name}". Проверьте название.`;
|
|
@@ -8048,6 +8136,23 @@ async function callOpenAiCompatible(config, messages, apiKey, providerName) {
|
|
|
8048
8136
|
throw new Error(`${providerName} API key не найден. Выполните iola ai key set ${providerName === "OpenAI" ? "openai" : "openrouter"} или задайте ${providerName === "OpenAI" ? "OPENAI_API_KEY" : "OPENROUTER_API_KEY"}.`);
|
|
8049
8137
|
}
|
|
8050
8138
|
|
|
8139
|
+
const networkMode = getAiNetworkMode(config);
|
|
8140
|
+
if (networkMode === "gateway") {
|
|
8141
|
+
return callAiRelay(config, messages, apiKey, providerName);
|
|
8142
|
+
}
|
|
8143
|
+
if (networkMode === "auto") {
|
|
8144
|
+
try {
|
|
8145
|
+
return await callOpenAiDirect(config, messages, apiKey, providerName);
|
|
8146
|
+
} catch (error) {
|
|
8147
|
+
if (!isNetworkFallbackError(error)) throw error;
|
|
8148
|
+
return callAiRelay(config, messages, apiKey, providerName);
|
|
8149
|
+
}
|
|
8150
|
+
}
|
|
8151
|
+
|
|
8152
|
+
return callOpenAiDirect(config, messages, apiKey, providerName);
|
|
8153
|
+
}
|
|
8154
|
+
|
|
8155
|
+
async function callOpenAiDirect(config, messages, apiKey, providerName) {
|
|
8051
8156
|
const response = await fetch(`${config.baseUrl}/chat/completions`, {
|
|
8052
8157
|
method: "POST",
|
|
8053
8158
|
headers: {
|
|
@@ -8065,13 +8170,80 @@ async function callOpenAiCompatible(config, messages, apiKey, providerName) {
|
|
|
8065
8170
|
|
|
8066
8171
|
if (!response.ok) {
|
|
8067
8172
|
const text = await response.text();
|
|
8068
|
-
throw new Error(`${providerName} request failed: ${response.status} ${response.statusText}\n${text}`);
|
|
8173
|
+
throw new Error(`${providerName} request failed: ${response.status} ${response.statusText}\n${sanitizeSecretFromText(text, apiKey)}`);
|
|
8174
|
+
}
|
|
8175
|
+
|
|
8176
|
+
const payload = await response.json();
|
|
8177
|
+
return payload.choices?.[0]?.message?.content || "";
|
|
8178
|
+
}
|
|
8179
|
+
|
|
8180
|
+
async function callAiRelay(config, messages, apiKey, providerName) {
|
|
8181
|
+
const relayBaseUrl = String(config.aiRelayBaseUrl || AI_RELAY_BASE_URL).replace(/\/+$/, "");
|
|
8182
|
+
const response = await fetch(`${relayBaseUrl}/chat`, {
|
|
8183
|
+
method: "POST",
|
|
8184
|
+
headers: {
|
|
8185
|
+
"content-type": "application/json",
|
|
8186
|
+
},
|
|
8187
|
+
body: JSON.stringify({
|
|
8188
|
+
provider: providerName === "OpenAI" ? "openai" : "openrouter",
|
|
8189
|
+
api_key: apiKey,
|
|
8190
|
+
model: config.model,
|
|
8191
|
+
messages,
|
|
8192
|
+
temperature: Number(config.temperature ?? 0.2),
|
|
8193
|
+
}),
|
|
8194
|
+
});
|
|
8195
|
+
|
|
8196
|
+
if (!response.ok) {
|
|
8197
|
+
const text = await response.text();
|
|
8198
|
+
throw new Error(`${providerName} relay request failed: ${response.status} ${response.statusText}\n${sanitizeSecretFromText(text, apiKey)}`);
|
|
8069
8199
|
}
|
|
8070
8200
|
|
|
8071
8201
|
const payload = await response.json();
|
|
8072
8202
|
return payload.choices?.[0]?.message?.content || "";
|
|
8073
8203
|
}
|
|
8074
8204
|
|
|
8205
|
+
async function callAiRelayModels(config, apiKey, providerName) {
|
|
8206
|
+
const relayBaseUrl = String(config.aiRelayBaseUrl || AI_RELAY_BASE_URL).replace(/\/+$/, "");
|
|
8207
|
+
const response = await fetch(`${relayBaseUrl}/models`, {
|
|
8208
|
+
method: "POST",
|
|
8209
|
+
headers: {
|
|
8210
|
+
"content-type": "application/json",
|
|
8211
|
+
},
|
|
8212
|
+
body: JSON.stringify({
|
|
8213
|
+
provider: providerName === "OpenAI" ? "openai" : "openrouter",
|
|
8214
|
+
api_key: apiKey,
|
|
8215
|
+
}),
|
|
8216
|
+
});
|
|
8217
|
+
|
|
8218
|
+
if (!response.ok) {
|
|
8219
|
+
const text = await response.text();
|
|
8220
|
+
throw new Error(`${providerName} relay models request failed: ${response.status} ${response.statusText}\n${sanitizeSecretFromText(text, apiKey)}`);
|
|
8221
|
+
}
|
|
8222
|
+
|
|
8223
|
+
return response.json();
|
|
8224
|
+
}
|
|
8225
|
+
|
|
8226
|
+
function getAiNetworkMode(config = {}) {
|
|
8227
|
+
return validateAiNetworkMode(AI_NETWORK_MODE || config.networkMode || "gateway");
|
|
8228
|
+
}
|
|
8229
|
+
|
|
8230
|
+
function validateAiNetworkMode(value) {
|
|
8231
|
+
const mode = String(value || "").trim().toLocaleLowerCase("en-US");
|
|
8232
|
+
if (mode === "direct" || mode === "gateway" || mode === "auto") return mode;
|
|
8233
|
+
throw new Error("AI network mode должен быть direct, gateway или auto.");
|
|
8234
|
+
}
|
|
8235
|
+
|
|
8236
|
+
function isNetworkFallbackError(error) {
|
|
8237
|
+
const message = String(error?.message || "");
|
|
8238
|
+
return /fetch failed|ECONN|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|socket|TLS|proxy|network/i.test(message);
|
|
8239
|
+
}
|
|
8240
|
+
|
|
8241
|
+
function sanitizeSecretFromText(text, secret) {
|
|
8242
|
+
const source = String(text || "");
|
|
8243
|
+
if (!secret) return source;
|
|
8244
|
+
return source.split(secret).join("[redacted]");
|
|
8245
|
+
}
|
|
8246
|
+
|
|
8075
8247
|
async function getApiKey(provider) {
|
|
8076
8248
|
if (provider === "openai" && process.env.OPENAI_API_KEY) {
|
|
8077
8249
|
return process.env.OPENAI_API_KEY;
|
|
@@ -10391,6 +10563,8 @@ function mergeConfig(base, override) {
|
|
|
10391
10563
|
|
|
10392
10564
|
function sanitizeConfig(config) {
|
|
10393
10565
|
const next = JSON.parse(JSON.stringify(config || {}));
|
|
10566
|
+
next.api = next.api || {};
|
|
10567
|
+
next.api.aiRelayBaseUrl = next.api.aiRelayBaseUrl || AI_RELAY_BASE_URL;
|
|
10394
10568
|
if (next.permissions?.localTools && typeof next.permissions.localTools === "object") {
|
|
10395
10569
|
for (const tool of Object.keys(next.permissions.localTools)) {
|
|
10396
10570
|
if (!ALL_TOOL_ALIASES.includes(tool)) {
|
|
@@ -10416,6 +10590,11 @@ function sanitizeConfig(config) {
|
|
|
10416
10590
|
next.ai.model = IOLA_LOCAL_MODEL;
|
|
10417
10591
|
next.ai.baseUrl = next.ai.baseUrl || "http://127.0.0.1:11434";
|
|
10418
10592
|
}
|
|
10593
|
+
for (const profile of Object.values(next.ai?.profiles || {})) {
|
|
10594
|
+
if (profile?.provider === "openai" || profile?.provider === "openrouter") {
|
|
10595
|
+
profile.networkMode = profile.networkMode || "gateway";
|
|
10596
|
+
}
|
|
10597
|
+
}
|
|
10419
10598
|
return next;
|
|
10420
10599
|
}
|
|
10421
10600
|
|
|
@@ -10429,6 +10608,7 @@ function validateConfig(config) {
|
|
|
10429
10608
|
for (const [name, profile] of Object.entries(config.ai?.profiles || {})) {
|
|
10430
10609
|
if (!["iola", "ollama", "openai", "openrouter", "codex"].includes(profile.provider)) errors.push(`ai.profiles.${name}.provider неизвестен`);
|
|
10431
10610
|
if (profile.provider !== "codex" && profile.provider !== "iola" && !profile.baseUrl) errors.push(`ai.profiles.${name}.baseUrl обязателен`);
|
|
10611
|
+
if (profile.networkMode && !["direct", "gateway", "auto"].includes(profile.networkMode)) errors.push(`ai.profiles.${name}.networkMode должен быть direct, gateway или auto`);
|
|
10432
10612
|
}
|
|
10433
10613
|
for (const tool of Object.keys(config.permissions?.localTools || {})) {
|
|
10434
10614
|
if (!ALL_TOOL_ALIASES.includes(tool)) errors.push(`permissions.localTools.${tool} неизвестен`);
|