@iola_adm/iola-cli 0.1.108 → 0.1.110

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.1.108",
3
+ "version": "0.1.110",
4
4
  "description": "CLI и AI-агент городского округа Йошкар-Ола.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/adm-iola/iola-cli#readme",
package/src/cli.js CHANGED
@@ -101,6 +101,19 @@ const FEATURES = {
101
101
  "mcp-management": { stage: "stable", defaultEnabled: true, description: "Команды управления MCP-интеграциями." },
102
102
  "web-search": { stage: "experimental", defaultEnabled: false, description: "Резерв под web-search режимы AI." },
103
103
  };
104
+ const MAIN_OPENROUTER_DEVELOPERS = [
105
+ ["openai", "OpenAI"],
106
+ ["anthropic", "Anthropic"],
107
+ ["google", "Google"],
108
+ ["qwen", "Qwen / Alibaba"],
109
+ ["deepseek", "DeepSeek"],
110
+ ["meta-llama", "Meta / Llama"],
111
+ ["mistralai", "Mistral AI"],
112
+ ["x-ai", "xAI"],
113
+ ["cohere", "Cohere"],
114
+ ["microsoft", "Microsoft"],
115
+ ["perplexity", "Perplexity"],
116
+ ];
104
117
  const SKILL_BUNDLES = {
105
118
  analyst: {
106
119
  description: "Аналитик открытых данных: поиск, карточки, отчеты и память.",
@@ -4288,6 +4301,7 @@ async function aiModels(args) {
4288
4301
  printTable(filtered, [
4289
4302
  ["id", "Модель"],
4290
4303
  ["provider", "Провайдер"],
4304
+ ["releaseDate", "Дата"],
4291
4305
  ["note", "Примечание"],
4292
4306
  ]);
4293
4307
  }
@@ -4361,11 +4375,7 @@ async function listAiModels(provider) {
4361
4375
  if (apiKey && getAiNetworkMode(relayConfig) === "gateway") {
4362
4376
  const payload = await callAiRelayModels(relayConfig, apiKey, "OpenRouter");
4363
4377
  return (payload.data || [])
4364
- .map((model) => ({
4365
- id: model.id,
4366
- provider: "openrouter",
4367
- note: model.name || "",
4368
- }))
4378
+ .map(mapOpenRouterModel)
4369
4379
  .sort((left, right) => left.id.localeCompare(right.id));
4370
4380
  }
4371
4381
  const response = await fetch("https://openrouter.ai/api/v1/models", {
@@ -4378,11 +4388,7 @@ async function listAiModels(provider) {
4378
4388
 
4379
4389
  const payload = await response.json();
4380
4390
  return (payload.data || [])
4381
- .map((model) => ({
4382
- id: model.id,
4383
- provider: "openrouter",
4384
- note: model.name || "",
4385
- }))
4391
+ .map(mapOpenRouterModel)
4386
4392
  .sort((left, right) => left.id.localeCompare(right.id));
4387
4393
  }
4388
4394
 
@@ -4405,6 +4411,77 @@ async function getApiProviderNetworkConfig(provider) {
4405
4411
  };
4406
4412
  }
4407
4413
 
4414
+ function mapOpenRouterModel(model) {
4415
+ const id = String(model.id || "");
4416
+ const architecture = model.architecture || {};
4417
+ const created = Number(model.created || 0);
4418
+ const developer = id.includes("/") ? id.split("/")[0] : "";
4419
+ return {
4420
+ id,
4421
+ provider: "openrouter",
4422
+ note: model.name || "",
4423
+ developer,
4424
+ created,
4425
+ releaseDate: formatUnixDate(created),
4426
+ modality: architecture.modality || "",
4427
+ inputModalities: Array.isArray(architecture.input_modalities) ? architecture.input_modalities : [],
4428
+ outputModalities: Array.isArray(architecture.output_modalities) ? architecture.output_modalities : [],
4429
+ contextLength: Number(model.context_length || model.top_provider?.context_length || 0),
4430
+ };
4431
+ }
4432
+
4433
+ function isOpenRouterTextGenerationModel(model) {
4434
+ const inputs = model.inputModalities || [];
4435
+ const outputs = model.outputModalities || [];
4436
+ if (inputs.length > 0 && !inputs.includes("text")) return false;
4437
+ if (outputs.length > 0 && !outputs.includes("text")) return false;
4438
+ if (outputs.includes("image") || outputs.includes("audio") || outputs.includes("video")) return false;
4439
+ const id = String(model.id || "").toLocaleLowerCase("en-US");
4440
+ const note = String(model.note || "").toLocaleLowerCase("en-US");
4441
+ return !/\b(image|video|audio|tts|embed|embedding|rerank|moderation|safeguard)\b/.test(`${id} ${note}`);
4442
+ }
4443
+
4444
+ function buildOpenRouterDeveloperChoices(models) {
4445
+ const byDeveloper = new Map();
4446
+ for (const model of models) {
4447
+ if (!model.developer) continue;
4448
+ const current = byDeveloper.get(model.developer) || { count: 0 };
4449
+ current.count += 1;
4450
+ byDeveloper.set(model.developer, current);
4451
+ }
4452
+
4453
+ return MAIN_OPENROUTER_DEVELOPERS
4454
+ .map(([id, label]) => {
4455
+ const stat = byDeveloper.get(id);
4456
+ if (!stat) return null;
4457
+ return {
4458
+ id,
4459
+ label,
4460
+ count: stat.count,
4461
+ };
4462
+ })
4463
+ .filter(Boolean);
4464
+ }
4465
+
4466
+ function sortModelsByFreshness(left, right) {
4467
+ return Number(right.created || 0) - Number(left.created || 0)
4468
+ || String(left.id).localeCompare(String(right.id));
4469
+ }
4470
+
4471
+ function formatUnixDate(value) {
4472
+ const seconds = Number(value || 0);
4473
+ if (!seconds) return "";
4474
+ return new Date(seconds * 1000).toISOString().slice(0, 10);
4475
+ }
4476
+
4477
+ function formatCompactNumber(value) {
4478
+ const number = Number(value || 0);
4479
+ if (!number) return "";
4480
+ if (number >= 1_000_000) return `${Math.round(number / 100_000) / 10}M`;
4481
+ if (number >= 1_000) return `${Math.round(number / 100) / 10}K`;
4482
+ return String(number);
4483
+ }
4484
+
4408
4485
  function getRecommendedOllamaModels(notePrefix = "recommended") {
4409
4486
  return [
4410
4487
  { id: IOLA_LOCAL_OLLAMA_MODEL, provider: "ollama", note: `${notePrefix} IOLA default low RAM` },
@@ -4711,8 +4788,12 @@ async function getDefaultApiProviderForModelSwitch() {
4711
4788
  }
4712
4789
 
4713
4790
  async function chooseAiModel(provider) {
4791
+ if (provider === "openrouter") {
4792
+ return chooseOpenRouterModel();
4793
+ }
4794
+
4714
4795
  let search = "";
4715
- if (provider === "openrouter" || provider === "openai") {
4796
+ if (provider === "openai") {
4716
4797
  search = (await askText("Фильтр моделей (Enter - без фильтра): ")).trim();
4717
4798
  }
4718
4799
 
@@ -4747,6 +4828,58 @@ async function chooseAiModel(provider) {
4747
4828
  return filtered[answer - 1]?.id || "";
4748
4829
  }
4749
4830
 
4831
+ async function chooseOpenRouterModel() {
4832
+ let models;
4833
+ try {
4834
+ models = await listAiModels("openrouter");
4835
+ } catch (error) {
4836
+ console.log(error instanceof Error ? error.message : String(error));
4837
+ return "";
4838
+ }
4839
+
4840
+ const textModels = models.filter(isOpenRouterTextGenerationModel);
4841
+ if (textModels.length === 0) {
4842
+ console.log("Текстовые модели OpenRouter не найдены.");
4843
+ return "";
4844
+ }
4845
+
4846
+ while (true) {
4847
+ const developerChoices = buildOpenRouterDeveloperChoices(textModels);
4848
+ console.log("Выберите разработчика моделей OpenRouter:");
4849
+ developerChoices.forEach((choice, index) => {
4850
+ console.log(` ${index + 1}. ${choice.label} (${choice.count})`);
4851
+ });
4852
+ console.log(" 0. Отмена");
4853
+
4854
+ const developerAnswer = Number(await askText("Номер: "));
4855
+ if (!developerAnswer) return "";
4856
+
4857
+ const selectedDeveloper = developerChoices[developerAnswer - 1];
4858
+ if (!selectedDeveloper) continue;
4859
+ const filtered = textModels
4860
+ .filter((model) => model.developer === selectedDeveloper.id)
4861
+ .sort(sortModelsByFreshness)
4862
+ .slice(0, 30);
4863
+
4864
+ if (filtered.length === 0) {
4865
+ console.log("Модели не найдены.");
4866
+ continue;
4867
+ }
4868
+
4869
+ console.log("Выберите текстовую модель:");
4870
+ filtered.forEach((model, index) => {
4871
+ const date = model.releaseDate || "дата неизвестна";
4872
+ const context = model.contextLength ? `, ctx ${formatCompactNumber(model.contextLength)}` : "";
4873
+ console.log(` ${index + 1}. ${model.id} (${date}${context}) - ${model.note || model.id}`);
4874
+ });
4875
+ console.log(" 0. Назад");
4876
+
4877
+ const modelAnswer = Number(await askText("Номер: "));
4878
+ if (!modelAnswer) continue;
4879
+ return filtered[modelAnswer - 1]?.id || "";
4880
+ }
4881
+ }
4882
+
4750
4883
  async function chooseAndSaveApiModel(provider) {
4751
4884
  const model = await chooseAiModel(provider);
4752
4885
  if (!model) {
@@ -47,6 +47,9 @@ assertIncludes(help, "iola master", "help");
47
47
  assertIncludes(help, "iola ask", "help");
48
48
 
49
49
  assertIncludes(cliSource, "force: Boolean(options.force)", "IOLA setup should not force model reinstall by default");
50
+ assertIncludes(cliSource, "MAIN_OPENROUTER_DEVELOPERS", "OpenRouter model selection should group models by developer");
51
+ assertIncludes(cliSource, "isOpenRouterTextGenerationModel", "OpenRouter model selection should prefer text-generation models");
52
+ assertIncludes(cliSource, "console.log(\" 0. Назад\")", "OpenRouter model selection should return to developer menu");
50
53
 
51
54
  const commands = await runCli(["commands"]);
52
55
  assertIncludes(commands, "iola browser status|install|open|text|html|screenshot|pdf|click|type|eval", "commands");