iosm-cli 0.2.1 → 0.2.3

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.
Files changed (31) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +8 -7
  3. package/dist/core/model-registry.d.ts.map +1 -1
  4. package/dist/core/model-registry.js +2 -3
  5. package/dist/core/model-registry.js.map +1 -1
  6. package/dist/core/models-dev-provider-catalog.d.ts +30 -0
  7. package/dist/core/models-dev-provider-catalog.d.ts.map +1 -0
  8. package/dist/core/models-dev-provider-catalog.js +118 -0
  9. package/dist/core/models-dev-provider-catalog.js.map +1 -0
  10. package/dist/core/models-dev-providers.d.ts +12 -0
  11. package/dist/core/models-dev-providers.d.ts.map +1 -0
  12. package/dist/core/models-dev-providers.js +736 -0
  13. package/dist/core/models-dev-providers.js.map +1 -0
  14. package/dist/core/sdk.d.ts.map +1 -1
  15. package/dist/core/sdk.js +62 -0
  16. package/dist/core/sdk.js.map +1 -1
  17. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  18. package/dist/modes/interactive/components/footer.js +3 -11
  19. package/dist/modes/interactive/components/footer.js.map +1 -1
  20. package/dist/modes/interactive/components/oauth-selector.d.ts +13 -1
  21. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  22. package/dist/modes/interactive/components/oauth-selector.js +89 -27
  23. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  24. package/dist/modes/interactive/interactive-mode.d.ts +13 -0
  25. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  26. package/dist/modes/interactive/interactive-mode.js +261 -21
  27. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  28. package/docs/configuration.md +4 -1
  29. package/docs/getting-started.md +2 -2
  30. package/docs/interactive-mode.md +1 -1
  31. package/package.json +2 -2
@@ -15,7 +15,9 @@ import { parseSkillBlock } from "../../core/agent-session.js";
15
15
  import { FooterDataProvider } from "../../core/footer-data-provider.js";
16
16
  import { KeybindingsManager } from "../../core/keybindings.js";
17
17
  import { createCompactionSummaryMessage, INTERNAL_UI_META_CUSTOM_TYPE, isInternalUiMetaDetails, } from "../../core/messages.js";
18
+ import { loadModelsDevProviderCatalog, } from "../../core/models-dev-provider-catalog.js";
18
19
  import { ModelRegistry } from "../../core/model-registry.js";
20
+ import { MODELS_DEV_PROVIDERS } from "../../core/models-dev-providers.js";
19
21
  import { resolveModelScope } from "../../core/model-resolver.js";
20
22
  import { getMcpCommandHelp, parseMcpAddCommand, parseMcpTargetCommand, } from "../../core/mcp/index.js";
21
23
  import { addMemoryEntry, getMemoryFilePath, readMemoryEntries, removeMemoryEntry, updateMemoryEntry, } from "../../core/memory.js";
@@ -272,6 +274,41 @@ function resolveDoctorCliToolStatuses() {
272
274
  });
273
275
  }
274
276
  const OPENROUTER_PROVIDER_ID = "openrouter";
277
+ const PROVIDER_DISPLAY_NAME_OVERRIDES = {
278
+ "azure-openai-responses": "Azure OpenAI Responses",
279
+ "google-antigravity": "Google Antigravity",
280
+ "google-gemini-cli": "Google Gemini CLI",
281
+ "kimi-coding": "Kimi Coding",
282
+ "openai-codex": "OpenAI Codex",
283
+ "opencode-go": "OpenCode Go",
284
+ "vercel-ai-gateway": "Vercel AI Gateway",
285
+ };
286
+ function toProviderDisplayName(providerId) {
287
+ const override = PROVIDER_DISPLAY_NAME_OVERRIDES[providerId];
288
+ if (override)
289
+ return override;
290
+ return providerId
291
+ .split(/[-_]/g)
292
+ .map((part) => {
293
+ const lower = part.toLowerCase();
294
+ if (lower === "ai")
295
+ return "AI";
296
+ if (lower === "api")
297
+ return "API";
298
+ if (lower === "gpt")
299
+ return "GPT";
300
+ if (lower === "aws")
301
+ return "AWS";
302
+ if (lower === "ui")
303
+ return "UI";
304
+ if (lower === "llm")
305
+ return "LLM";
306
+ if (lower === "id")
307
+ return "ID";
308
+ return part.charAt(0).toUpperCase() + part.slice(1);
309
+ })
310
+ .join(" ");
311
+ }
275
312
  function isAbortLikeMessage(message) {
276
313
  const normalized = message.trim().toLowerCase();
277
314
  return normalized.includes("aborted") || normalized.includes("cancelled");
@@ -421,6 +458,17 @@ export class InteractiveMode {
421
458
  this.builtInHeader = undefined;
422
459
  // ASCII logo component for startup screen
423
460
  this.asciiLogo = undefined;
461
+ // API-key provider labels cached for /login and status messages.
462
+ this.apiKeyProviderDisplayNames = new Map();
463
+ this.modelsDevProviderCatalog = MODELS_DEV_PROVIDERS;
464
+ this.modelsDevProviderCatalogById = new Map(MODELS_DEV_PROVIDERS.map((provider) => [
465
+ provider.id,
466
+ {
467
+ ...provider,
468
+ models: [],
469
+ },
470
+ ]));
471
+ this.modelsDevProviderCatalogRefreshPromise = undefined;
424
472
  // Custom header from extension (undefined = use built-in header)
425
473
  this.customHeader = undefined;
426
474
  /**
@@ -1197,6 +1245,8 @@ export class InteractiveMode {
1197
1245
  // Both are needed: fd for autocomplete, rg for grep tool and bash commands
1198
1246
  const [fdPath] = await Promise.all([ensureTool("fd"), ensureTool("rg")]);
1199
1247
  this.fdPath = fdPath;
1248
+ // Restore saved default model early so startup header/session are consistent after restart.
1249
+ await this.restoreSavedModelSelectionOnStartup();
1200
1250
  // Add header container as first child
1201
1251
  this.ui.addChild(this.headerContainer);
1202
1252
  // Add header with keybindings from config (unless silenced)
@@ -1275,6 +1325,8 @@ export class InteractiveMode {
1275
1325
  this.footerDataProvider.onBranchChange(() => {
1276
1326
  this.ui.requestRender();
1277
1327
  });
1328
+ // Refresh provider catalog from models.dev in background once per startup.
1329
+ void this.refreshModelsDevProviderCatalog();
1278
1330
  // Initialize available provider count for footer display
1279
1331
  await this.updateAvailableProviderCount();
1280
1332
  }
@@ -1431,7 +1483,10 @@ export class InteractiveMode {
1431
1483
  this.showError(`models.json error: ${modelsJsonError}`);
1432
1484
  }
1433
1485
  if (modelFallbackMessage) {
1434
- this.showWarning(modelFallbackMessage);
1486
+ const staleNoModelsWarning = this.session.model && modelFallbackMessage.startsWith("No models available.");
1487
+ if (!staleNoModelsWarning) {
1488
+ this.showWarning(modelFallbackMessage);
1489
+ }
1435
1490
  }
1436
1491
  if (!this.session.model) {
1437
1492
  this.showWarning("No model selected. Choose a model to start.");
@@ -3870,7 +3925,7 @@ export class InteractiveMode {
3870
3925
  this.updateEditorBorderColor();
3871
3926
  this.refreshBuiltInHeader();
3872
3927
  const thinkingStr = result.model.reasoning && result.thinkingLevel !== "off" ? ` (thinking: ${result.thinkingLevel})` : "";
3873
- this.showStatus(`Switched to ${result.model.name || result.model.id}${thinkingStr}`);
3928
+ this.showStatus(`Switched to ${result.model.provider}/${result.model.id}${thinkingStr}`);
3874
3929
  }
3875
3930
  }
3876
3931
  catch (error) {
@@ -4910,7 +4965,7 @@ export class InteractiveMode {
4910
4965
  this.footer.invalidate();
4911
4966
  this.updateEditorBorderColor();
4912
4967
  this.refreshBuiltInHeader();
4913
- this.showStatus(`Model: ${model.id}`);
4968
+ this.showStatus(`Model: ${model.provider}/${model.id}`);
4914
4969
  this.checkDaxnutsEasterEgg(model);
4915
4970
  }
4916
4971
  catch (error) {
@@ -4921,6 +4976,7 @@ export class InteractiveMode {
4921
4976
  this.showModelSelector(searchTerm);
4922
4977
  }
4923
4978
  async showModelProviderSelector(preferredProvider) {
4979
+ await this.hydrateMissingProviderModelsForSavedAuth();
4924
4980
  this.session.modelRegistry.refresh();
4925
4981
  let models = [];
4926
4982
  try {
@@ -5032,7 +5088,7 @@ export class InteractiveMode {
5032
5088
  this.updateEditorBorderColor();
5033
5089
  this.refreshBuiltInHeader();
5034
5090
  done();
5035
- this.showStatus(`Model: ${model.id}`);
5091
+ this.showStatus(`Model: ${model.provider}/${model.id}`);
5036
5092
  this.checkDaxnutsEasterEgg(model);
5037
5093
  }
5038
5094
  catch (error) {
@@ -5341,22 +5397,29 @@ export class InteractiveMode {
5341
5397
  return;
5342
5398
  }
5343
5399
  }
5400
+ await this.refreshModelsDevProviderCatalog();
5401
+ const apiKeyProviders = this.getApiKeyLoginProviders(this.modelsDevProviderCatalog);
5344
5402
  this.showSelector((done) => {
5345
- const selector = new OAuthSelectorComponent(mode, this.session.modelRegistry.authStorage, async (providerId) => {
5403
+ const selector = new OAuthSelectorComponent(mode, this.session.modelRegistry.authStorage, async (provider) => {
5346
5404
  done();
5347
5405
  if (mode === "login") {
5348
- if (providerId === OPENROUTER_PROVIDER_ID) {
5349
- await this.handleOpenRouterApiKeyLogin();
5406
+ if (provider.kind === "api_key") {
5407
+ if (provider.id === OPENROUTER_PROVIDER_ID) {
5408
+ await this.handleOpenRouterApiKeyLogin();
5409
+ }
5410
+ else {
5411
+ await this.handleApiKeyLogin(provider.id, { providerName: provider.name });
5412
+ }
5350
5413
  }
5351
5414
  else {
5352
- await this.showLoginDialog(providerId);
5415
+ await this.showLoginDialog(provider.id);
5353
5416
  }
5354
5417
  }
5355
5418
  else {
5356
5419
  // Logout flow
5357
- const providerName = this.getProviderDisplayName(providerId);
5420
+ const providerName = this.getProviderDisplayName(provider.id);
5358
5421
  try {
5359
- this.session.modelRegistry.authStorage.logout(providerId);
5422
+ this.session.modelRegistry.authStorage.logout(provider.id);
5360
5423
  this.session.modelRegistry.refresh();
5361
5424
  await this.updateAvailableProviderCount();
5362
5425
  this.showStatus(`Logged out of ${providerName}`);
@@ -5368,21 +5431,179 @@ export class InteractiveMode {
5368
5431
  }, () => {
5369
5432
  done();
5370
5433
  this.ui.requestRender();
5371
- });
5434
+ }, apiKeyProviders);
5372
5435
  return { component: selector, focus: selector };
5373
5436
  });
5374
5437
  }
5438
+ async refreshModelsDevProviderCatalog() {
5439
+ if (this.modelsDevProviderCatalogRefreshPromise) {
5440
+ await this.modelsDevProviderCatalogRefreshPromise;
5441
+ return;
5442
+ }
5443
+ this.modelsDevProviderCatalogRefreshPromise = (async () => {
5444
+ const catalog = await loadModelsDevProviderCatalog();
5445
+ this.modelsDevProviderCatalogById = catalog;
5446
+ this.modelsDevProviderCatalog = Array.from(catalog.values())
5447
+ .map((provider) => ({
5448
+ id: provider.id,
5449
+ name: provider.name,
5450
+ env: provider.env,
5451
+ }))
5452
+ .sort((a, b) => a.name.localeCompare(b.name, "en") || a.id.localeCompare(b.id, "en"));
5453
+ })()
5454
+ .catch(() => {
5455
+ this.modelsDevProviderCatalog = MODELS_DEV_PROVIDERS;
5456
+ this.modelsDevProviderCatalogById = new Map(MODELS_DEV_PROVIDERS.map((provider) => [
5457
+ provider.id,
5458
+ {
5459
+ ...provider,
5460
+ models: [],
5461
+ },
5462
+ ]));
5463
+ })
5464
+ .finally(() => {
5465
+ this.modelsDevProviderCatalogRefreshPromise = undefined;
5466
+ });
5467
+ await this.modelsDevProviderCatalogRefreshPromise;
5468
+ }
5469
+ resolveModelsDevApi(modelNpm) {
5470
+ const npm = modelNpm?.toLowerCase() ?? "";
5471
+ if (npm.includes("anthropic"))
5472
+ return "anthropic-messages";
5473
+ if (npm.includes("google-vertex"))
5474
+ return "google-vertex";
5475
+ if (npm.includes("google"))
5476
+ return "google-generative-ai";
5477
+ if (npm.includes("amazon-bedrock"))
5478
+ return "bedrock-converse-stream";
5479
+ if (npm.includes("mistral"))
5480
+ return "mistral-conversations";
5481
+ if (npm.includes("@ai-sdk/openai") && !npm.includes("compatible"))
5482
+ return "openai-responses";
5483
+ return "openai-completions";
5484
+ }
5485
+ buildModelsDevProviderConfig(providerInfo) {
5486
+ const baseUrl = providerInfo.api ?? providerInfo.models.find((model) => !!model.api)?.api;
5487
+ if (!baseUrl)
5488
+ return undefined;
5489
+ if (providerInfo.models.length === 0)
5490
+ return undefined;
5491
+ const models = providerInfo.models.map((model) => ({
5492
+ id: model.id,
5493
+ name: model.name,
5494
+ api: this.resolveModelsDevApi(model.npm ?? providerInfo.npm),
5495
+ reasoning: model.reasoning,
5496
+ input: [...model.input],
5497
+ cost: model.cost,
5498
+ contextWindow: model.contextWindow,
5499
+ maxTokens: model.maxTokens,
5500
+ headers: Object.keys(model.headers).length > 0 ? model.headers : undefined,
5501
+ }));
5502
+ return {
5503
+ baseUrl,
5504
+ models,
5505
+ };
5506
+ }
5507
+ hasRegisteredProviderModels(providerId) {
5508
+ const registry = this.session.modelRegistry;
5509
+ if (typeof registry.getAll !== "function")
5510
+ return true;
5511
+ return registry.getAll().some((model) => model.provider === providerId);
5512
+ }
5513
+ async hydrateProviderModelsFromModelsDev(providerId) {
5514
+ if (this.hasRegisteredProviderModels(providerId))
5515
+ return true;
5516
+ await this.refreshModelsDevProviderCatalog();
5517
+ const providerInfo = this.modelsDevProviderCatalogById.get(providerId);
5518
+ if (!providerInfo)
5519
+ return false;
5520
+ const config = this.buildModelsDevProviderConfig(providerInfo);
5521
+ if (!config)
5522
+ return false;
5523
+ try {
5524
+ this.session.modelRegistry.registerProvider(providerId, config);
5525
+ return this.hasRegisteredProviderModels(providerId);
5526
+ }
5527
+ catch {
5528
+ return false;
5529
+ }
5530
+ }
5531
+ async hydrateMissingProviderModelsForSavedAuth() {
5532
+ const savedProviders = this.session.modelRegistry.authStorage.list();
5533
+ if (savedProviders.length === 0)
5534
+ return;
5535
+ for (const providerId of savedProviders) {
5536
+ if (this.hasRegisteredProviderModels(providerId))
5537
+ continue;
5538
+ await this.hydrateProviderModelsFromModelsDev(providerId);
5539
+ }
5540
+ }
5541
+ async restoreSavedModelSelectionOnStartup() {
5542
+ if (this.session.model)
5543
+ return;
5544
+ const defaultProvider = this.settingsManager.getDefaultProvider();
5545
+ const defaultModelId = this.settingsManager.getDefaultModel();
5546
+ if (!defaultProvider || !defaultModelId)
5547
+ return;
5548
+ if (!this.hasRegisteredProviderModels(defaultProvider)) {
5549
+ await this.hydrateProviderModelsFromModelsDev(defaultProvider);
5550
+ }
5551
+ const model = this.session.modelRegistry.find(defaultProvider, defaultModelId);
5552
+ if (!model)
5553
+ return;
5554
+ try {
5555
+ const apiKey = await this.session.modelRegistry.getApiKey(model);
5556
+ if (!apiKey)
5557
+ return;
5558
+ }
5559
+ catch {
5560
+ return;
5561
+ }
5562
+ this.session.agent.setModel(model);
5563
+ }
5564
+ getApiKeyLoginProviders(modelsDevProviders) {
5565
+ const providerNames = new Map();
5566
+ this.apiKeyProviderDisplayNames.clear();
5567
+ for (const model of this.session.modelRegistry.getAll()) {
5568
+ if (!providerNames.has(model.provider)) {
5569
+ providerNames.set(model.provider, toProviderDisplayName(model.provider));
5570
+ }
5571
+ }
5572
+ for (const provider of modelsDevProviders) {
5573
+ const fallbackName = toProviderDisplayName(provider.id);
5574
+ const current = providerNames.get(provider.id);
5575
+ if (!current || current === fallbackName) {
5576
+ providerNames.set(provider.id, provider.name || fallbackName);
5577
+ }
5578
+ }
5579
+ for (const providerId of this.session.modelRegistry.authStorage.list()) {
5580
+ if (!providerNames.has(providerId)) {
5581
+ providerNames.set(providerId, toProviderDisplayName(providerId));
5582
+ }
5583
+ }
5584
+ const oauthProviderIds = new Set(this.session.modelRegistry.authStorage.getOAuthProviders().map((provider) => provider.id));
5585
+ const providers = [];
5586
+ for (const [id, name] of providerNames.entries()) {
5587
+ if (oauthProviderIds.has(id))
5588
+ continue;
5589
+ this.apiKeyProviderDisplayNames.set(id, name);
5590
+ providers.push({ id, name, kind: "api_key" });
5591
+ }
5592
+ providers.sort((a, b) => a.name.localeCompare(b.name));
5593
+ return providers;
5594
+ }
5375
5595
  getProviderDisplayName(providerId) {
5376
- if (providerId === OPENROUTER_PROVIDER_ID) {
5377
- return "OpenRouter";
5596
+ const apiKeyName = this.apiKeyProviderDisplayNames.get(providerId);
5597
+ if (apiKeyName) {
5598
+ return apiKeyName;
5378
5599
  }
5379
5600
  const providerInfo = this.session.modelRegistry.authStorage.getOAuthProviders().find((p) => p.id === providerId);
5380
- return providerInfo?.name || providerId;
5601
+ return providerInfo?.name || toProviderDisplayName(providerId);
5381
5602
  }
5382
- async handleOpenRouterApiKeyLogin(options) {
5603
+ async handleApiKeyLogin(providerId, options) {
5604
+ const providerName = options?.providerName || this.getProviderDisplayName(providerId);
5383
5605
  const openModelSelector = options?.openModelSelector ?? true;
5384
- const providerName = this.getProviderDisplayName(OPENROUTER_PROVIDER_ID);
5385
- const existingCredential = this.session.modelRegistry.authStorage.get(OPENROUTER_PROVIDER_ID);
5606
+ const existingCredential = this.session.modelRegistry.authStorage.get(providerId);
5386
5607
  if (existingCredential) {
5387
5608
  const overwrite = await this.showExtensionConfirm(`${providerName}: replace existing credentials?`, `Stored at ${getAuthPath()}`);
5388
5609
  if (!overwrite) {
@@ -5390,7 +5611,11 @@ export class InteractiveMode {
5390
5611
  return;
5391
5612
  }
5392
5613
  }
5393
- const keyInput = await this.showExtensionInput(`${providerName} API key\nCreate key: https://openrouter.ai/keys`, "sk-or-v1-...");
5614
+ const promptLines = [`${providerName} API key`];
5615
+ if (options?.createKeyUrl) {
5616
+ promptLines.push(`Create key: ${options.createKeyUrl}`);
5617
+ }
5618
+ const keyInput = await this.showExtensionInput(promptLines.join("\n"), options?.placeholder ?? "api-key");
5394
5619
  if (keyInput === undefined) {
5395
5620
  this.showStatus(`${providerName} login cancelled.`);
5396
5621
  return;
@@ -5400,13 +5625,28 @@ export class InteractiveMode {
5400
5625
  this.showWarning(`${providerName} API key cannot be empty.`);
5401
5626
  return;
5402
5627
  }
5403
- this.session.modelRegistry.authStorage.set(OPENROUTER_PROVIDER_ID, { type: "api_key", key: apiKey });
5628
+ this.session.modelRegistry.authStorage.set(providerId, { type: "api_key", key: apiKey });
5629
+ let hasProviderModels = this.hasRegisteredProviderModels(providerId);
5630
+ if (!hasProviderModels) {
5631
+ hasProviderModels = await this.hydrateProviderModelsFromModelsDev(providerId);
5632
+ }
5404
5633
  await this.updateAvailableProviderCount();
5405
5634
  this.showStatus(`${providerName} API key saved to ${getAuthPath()}`);
5406
- if (openModelSelector) {
5407
- await this.showModelProviderSelector(OPENROUTER_PROVIDER_ID);
5635
+ if (openModelSelector && hasProviderModels) {
5636
+ await this.showModelProviderSelector(providerId);
5637
+ }
5638
+ else if (openModelSelector) {
5639
+ this.showWarning(`${providerName} configured, but no models are available yet. Run /model after network is available.`);
5408
5640
  }
5409
5641
  }
5642
+ async handleOpenRouterApiKeyLogin(options) {
5643
+ await this.handleApiKeyLogin(OPENROUTER_PROVIDER_ID, {
5644
+ providerName: "OpenRouter",
5645
+ openModelSelector: options?.openModelSelector,
5646
+ createKeyUrl: "https://openrouter.ai/keys",
5647
+ placeholder: "sk-or-v1-...",
5648
+ });
5649
+ }
5410
5650
  async showLoginDialog(providerId) {
5411
5651
  const providerInfo = this.session.modelRegistry.authStorage.getOAuthProviders().find((p) => p.id === providerId);
5412
5652
  const providerName = this.getProviderDisplayName(providerId);