@iola_adm/iola-cli 0.1.112 → 0.2.0
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 +8 -0
- package/package.json +1 -1
- package/src/cli.js +89 -10
- package/test/smoke-test.js +4 -0
- package/wiki/AI-/320/277/321/200/320/276/321/204/320/270/320/273/320/270.md +8 -0
- 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 +2 -0
package/README.md
CHANGED
|
@@ -133,6 +133,14 @@ CLI использует модель `iola-router:qwen3-1.7b-v4-q8` из GGUF-
|
|
|
133
133
|
|
|
134
134
|
В интерактивном CLI команда `/model` переключает локальную модель IOLA, API-профили OpenAI/OpenRouter и Codex CLI. Для OpenRouter выбор устроен так: сначала выбирается разработчик моделей, затем CLI показывает до 30 самых свежих моделей для текстовой работы с датой релиза и размером контекста. В списке моделей `0` возвращает к выбору разработчика.
|
|
135
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
|
+
|
|
136
144
|
Ollama остается опциональным runtime:
|
|
137
145
|
|
|
138
146
|
```bash
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -4372,8 +4372,8 @@ async function listAiModels(provider) {
|
|
|
4372
4372
|
if (getAiNetworkMode(relayConfig) === "gateway") {
|
|
4373
4373
|
const payload = await callAiRelayModels(relayConfig, apiKey, "OpenAI");
|
|
4374
4374
|
return (payload.data || [])
|
|
4375
|
-
.map(
|
|
4376
|
-
.sort(
|
|
4375
|
+
.map(mapOpenAiModel)
|
|
4376
|
+
.sort(sortModelsByFreshness);
|
|
4377
4377
|
}
|
|
4378
4378
|
const response = await fetch("https://api.openai.com/v1/models", {
|
|
4379
4379
|
headers: { authorization: `Bearer ${apiKey}` },
|
|
@@ -4385,8 +4385,8 @@ async function listAiModels(provider) {
|
|
|
4385
4385
|
|
|
4386
4386
|
const payload = await response.json();
|
|
4387
4387
|
return (payload.data || [])
|
|
4388
|
-
.map(
|
|
4389
|
-
.sort(
|
|
4388
|
+
.map(mapOpenAiModel)
|
|
4389
|
+
.sort(sortModelsByFreshness);
|
|
4390
4390
|
}
|
|
4391
4391
|
|
|
4392
4392
|
if (provider === "openrouter") {
|
|
@@ -4431,6 +4431,44 @@ async function getApiProviderNetworkConfig(provider) {
|
|
|
4431
4431
|
};
|
|
4432
4432
|
}
|
|
4433
4433
|
|
|
4434
|
+
function mapOpenAiModel(model) {
|
|
4435
|
+
const id = String(model.id || "");
|
|
4436
|
+
const created = Number(model.created || inferOpenAiModelCreated(id) || 0);
|
|
4437
|
+
return {
|
|
4438
|
+
id,
|
|
4439
|
+
provider: "openai",
|
|
4440
|
+
note: model.owned_by || "",
|
|
4441
|
+
created,
|
|
4442
|
+
releaseDate: formatUnixDate(created),
|
|
4443
|
+
};
|
|
4444
|
+
}
|
|
4445
|
+
|
|
4446
|
+
function isOpenAiTextGenerationModel(model) {
|
|
4447
|
+
const id = String(model.id || "").toLocaleLowerCase("en-US");
|
|
4448
|
+
if (!id) return false;
|
|
4449
|
+
if (/^(babbage|davinci|text-|whisper|tts|dall-|omni-|computer-|codex-mini-latest)/.test(id)) return false;
|
|
4450
|
+
if (/(image|audio|tts|transcribe|realtime|embedding|moderation|search|instruct|safeguard)/.test(id)) return false;
|
|
4451
|
+
if (/^gpt-3\.5/.test(id)) return false;
|
|
4452
|
+
return id === "chat-latest"
|
|
4453
|
+
|| /^gpt-(4|4o|5|5\.)/.test(id)
|
|
4454
|
+
|| /^o[134](?:-|$)/.test(id);
|
|
4455
|
+
}
|
|
4456
|
+
|
|
4457
|
+
function dedupeDatedOpenAiModels(models) {
|
|
4458
|
+
const ids = new Set(models.map((model) => model.id));
|
|
4459
|
+
return models.filter((model) => {
|
|
4460
|
+
const base = model.id.replace(/-\d{4}-\d{2}-\d{2}$/, "");
|
|
4461
|
+
return base === model.id || !ids.has(base);
|
|
4462
|
+
});
|
|
4463
|
+
}
|
|
4464
|
+
|
|
4465
|
+
function inferOpenAiModelCreated(id) {
|
|
4466
|
+
const match = String(id || "").match(/(\d{4})-(\d{2})-(\d{2})$/);
|
|
4467
|
+
if (!match) return 0;
|
|
4468
|
+
const [, year, month, day] = match;
|
|
4469
|
+
return Math.floor(Date.UTC(Number(year), Number(month) - 1, Number(day)) / 1000);
|
|
4470
|
+
}
|
|
4471
|
+
|
|
4434
4472
|
function mapOpenRouterModel(model) {
|
|
4435
4473
|
const id = String(model.id || "");
|
|
4436
4474
|
const architecture = model.architecture || {};
|
|
@@ -4757,8 +4795,8 @@ async function chooseModelTarget() {
|
|
|
4757
4795
|
|
|
4758
4796
|
async function openModelTargetMenu(target) {
|
|
4759
4797
|
if (target === "local") {
|
|
4760
|
-
const
|
|
4761
|
-
if (model) await switchModelTarget(
|
|
4798
|
+
const selection = await chooseLocalModel();
|
|
4799
|
+
if (selection?.model) await switchModelTarget(selection.provider, selection.model);
|
|
4762
4800
|
return;
|
|
4763
4801
|
}
|
|
4764
4802
|
|
|
@@ -4807,6 +4845,37 @@ async function getDefaultApiProviderForModelSwitch() {
|
|
|
4807
4845
|
return apiProfile?.provider || "openai";
|
|
4808
4846
|
}
|
|
4809
4847
|
|
|
4848
|
+
async function chooseLocalModel() {
|
|
4849
|
+
const models = await listAiModels("ollama");
|
|
4850
|
+
const choices = [
|
|
4851
|
+
{ id: IOLA_LOCAL_MODEL, provider: "iola", label: `${IOLA_LOCAL_MODEL} - IOLA local router` },
|
|
4852
|
+
...models
|
|
4853
|
+
.filter((model) => model.id !== IOLA_LOCAL_MODEL)
|
|
4854
|
+
.map((model) => ({
|
|
4855
|
+
id: model.id,
|
|
4856
|
+
provider: "ollama",
|
|
4857
|
+
label: `${model.id}${model.note ? ` - ${model.note}` : ""}`,
|
|
4858
|
+
})),
|
|
4859
|
+
{ id: "__manual__", provider: "ollama", label: "Другая Ollama-модель: ввести имя вручную" },
|
|
4860
|
+
].filter((item, index, array) => array.findIndex((candidate) => candidate.id === item.id) === index);
|
|
4861
|
+
|
|
4862
|
+
console.log("Выберите локальную модель:");
|
|
4863
|
+
choices.forEach((choice, index) => console.log(` ${index + 1}. ${choice.label}`));
|
|
4864
|
+
console.log(" 0. Отмена");
|
|
4865
|
+
|
|
4866
|
+
const answer = Number(await askText("Номер: "));
|
|
4867
|
+
const selected = choices[answer - 1];
|
|
4868
|
+
if (!selected) return null;
|
|
4869
|
+
|
|
4870
|
+
if (selected.id === "__manual__") {
|
|
4871
|
+
const model = (await askText("Имя Ollama-модели, например qwen3:4b: ")).trim();
|
|
4872
|
+
if (!model) return null;
|
|
4873
|
+
return { provider: "ollama", model };
|
|
4874
|
+
}
|
|
4875
|
+
|
|
4876
|
+
return { provider: selected.provider, model: selected.id };
|
|
4877
|
+
}
|
|
4878
|
+
|
|
4810
4879
|
async function chooseAiModel(provider) {
|
|
4811
4880
|
if (provider === "openrouter") {
|
|
4812
4881
|
return chooseOpenRouterModel();
|
|
@@ -4834,19 +4903,27 @@ async function chooseAiModel(provider) {
|
|
|
4834
4903
|
? models.filter((model) => model.id.toLocaleLowerCase("ru-RU").includes(search.toLocaleLowerCase("ru-RU")))
|
|
4835
4904
|
: models;
|
|
4836
4905
|
|
|
4906
|
+
if (provider === "openai") {
|
|
4907
|
+
filtered = dedupeDatedOpenAiModels(filtered.filter(isOpenAiTextGenerationModel))
|
|
4908
|
+
.sort(sortModelsByFreshness);
|
|
4909
|
+
}
|
|
4910
|
+
|
|
4837
4911
|
if (filtered.length === 0) {
|
|
4838
4912
|
console.log("Модели не найдены.");
|
|
4839
4913
|
return "";
|
|
4840
4914
|
}
|
|
4841
4915
|
|
|
4842
|
-
const limit = 25;
|
|
4916
|
+
const limit = provider === "openai" ? 30 : 25;
|
|
4843
4917
|
if (filtered.length > limit) {
|
|
4844
4918
|
filtered = filtered.slice(0, limit);
|
|
4845
|
-
console.log(`Показаны первые ${limit}
|
|
4919
|
+
console.log(`Показаны первые ${limit} моделей.`);
|
|
4846
4920
|
}
|
|
4847
4921
|
|
|
4848
4922
|
console.log("Выберите модель:");
|
|
4849
|
-
filtered.forEach((model, index) =>
|
|
4923
|
+
filtered.forEach((model, index) => {
|
|
4924
|
+
const date = model.releaseDate ? ` (${model.releaseDate})` : "";
|
|
4925
|
+
console.log(` ${index + 1}. ${model.id}${date}${model.note ? ` - ${model.note}` : ""}`);
|
|
4926
|
+
});
|
|
4850
4927
|
console.log(" 0. Отмена");
|
|
4851
4928
|
|
|
4852
4929
|
const answer = Number(await askText("Номер: "));
|
|
@@ -4942,7 +5019,9 @@ async function switchModelTarget(target, model) {
|
|
|
4942
5019
|
if (!ready) return;
|
|
4943
5020
|
}
|
|
4944
5021
|
const profileName = provider === "ollama" || provider === "iola" ? "local" : provider;
|
|
4945
|
-
const currentProfile =
|
|
5022
|
+
const currentProfile = (provider === "ollama" || provider === "iola")
|
|
5023
|
+
? buildProfileFromOptions(provider, { model })
|
|
5024
|
+
: (config.ai.profiles?.[profileName] || buildProfileFromOptions(provider, { model }));
|
|
4946
5025
|
const profile = {
|
|
4947
5026
|
...currentProfile,
|
|
4948
5027
|
provider,
|
package/test/smoke-test.js
CHANGED
|
@@ -53,6 +53,10 @@ assertIncludes(cliSource, "console.log(\" 0. Назад\")", "OpenRouter model
|
|
|
53
53
|
assertIncludes(cliSource, "renderTerminalMarkdown", "AI answers should render inline markdown in the terminal");
|
|
54
54
|
assertIncludes(cliSource, "\\x1b[1m$1\\x1b[22m", "AI answer renderer should support bold markdown");
|
|
55
55
|
assertIncludes(cliSource, "ensureApiKeyForModelSelection", "API model selection should prompt for missing provider keys");
|
|
56
|
+
assertIncludes(cliSource, "isOpenAiTextGenerationModel", "OpenAI model selection should filter technical and legacy models");
|
|
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");
|
|
56
60
|
|
|
57
61
|
const commands = await runCli(["commands"]);
|
|
58
62
|
assertIncludes(commands, "iola browser status|install|open|text|html|screenshot|pdf|click|type|eval", "commands");
|
|
@@ -10,6 +10,14 @@ 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
|
+
|
|
13
21
|
## OpenAI
|
|
14
22
|
|
|
15
23
|
```bash
|
|
@@ -28,6 +28,8 @@ iola master
|
|
|
28
28
|
|
|
29
29
|
Если модель уже скачана и доступна, пункт показывается как `готово`.
|
|
30
30
|
|
|
31
|
+
После настройки локальную модель можно менять в интерактивном агенте через `/model`. Помимо штатной IOLA-модели можно выбрать установленную или рекомендуемую Ollama-модель, либо вручную ввести имя любой модели из библиотеки Ollama.
|
|
32
|
+
|
|
31
33
|
### 4. OpenAI API
|
|
32
34
|
|
|
33
35
|
Настраивает профиль OpenAI и сохраняет API-ключ локально у пользователя.
|