iosm-cli 0.2.1 → 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.
Files changed (28) hide show
  1. package/CHANGELOG.md +53 -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/modes/interactive/components/footer.d.ts.map +1 -1
  15. package/dist/modes/interactive/components/footer.js +3 -11
  16. package/dist/modes/interactive/components/footer.js.map +1 -1
  17. package/dist/modes/interactive/components/oauth-selector.d.ts +13 -1
  18. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  19. package/dist/modes/interactive/components/oauth-selector.js +89 -27
  20. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  21. package/dist/modes/interactive/interactive-mode.d.ts +12 -0
  22. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  23. package/dist/modes/interactive/interactive-mode.js +232 -20
  24. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  25. package/docs/configuration.md +4 -1
  26. package/docs/getting-started.md +2 -2
  27. package/docs/interactive-mode.md +1 -1
  28. 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
  /**
@@ -1275,6 +1323,8 @@ export class InteractiveMode {
1275
1323
  this.footerDataProvider.onBranchChange(() => {
1276
1324
  this.ui.requestRender();
1277
1325
  });
1326
+ // Refresh provider catalog from models.dev in background once per startup.
1327
+ void this.refreshModelsDevProviderCatalog();
1278
1328
  // Initialize available provider count for footer display
1279
1329
  await this.updateAvailableProviderCount();
1280
1330
  }
@@ -3870,7 +3920,7 @@ export class InteractiveMode {
3870
3920
  this.updateEditorBorderColor();
3871
3921
  this.refreshBuiltInHeader();
3872
3922
  const thinkingStr = result.model.reasoning && result.thinkingLevel !== "off" ? ` (thinking: ${result.thinkingLevel})` : "";
3873
- this.showStatus(`Switched to ${result.model.name || result.model.id}${thinkingStr}`);
3923
+ this.showStatus(`Switched to ${result.model.provider}/${result.model.id}${thinkingStr}`);
3874
3924
  }
3875
3925
  }
3876
3926
  catch (error) {
@@ -4910,7 +4960,7 @@ export class InteractiveMode {
4910
4960
  this.footer.invalidate();
4911
4961
  this.updateEditorBorderColor();
4912
4962
  this.refreshBuiltInHeader();
4913
- this.showStatus(`Model: ${model.id}`);
4963
+ this.showStatus(`Model: ${model.provider}/${model.id}`);
4914
4964
  this.checkDaxnutsEasterEgg(model);
4915
4965
  }
4916
4966
  catch (error) {
@@ -4921,6 +4971,7 @@ export class InteractiveMode {
4921
4971
  this.showModelSelector(searchTerm);
4922
4972
  }
4923
4973
  async showModelProviderSelector(preferredProvider) {
4974
+ await this.hydrateMissingProviderModelsForSavedAuth();
4924
4975
  this.session.modelRegistry.refresh();
4925
4976
  let models = [];
4926
4977
  try {
@@ -5032,7 +5083,7 @@ export class InteractiveMode {
5032
5083
  this.updateEditorBorderColor();
5033
5084
  this.refreshBuiltInHeader();
5034
5085
  done();
5035
- this.showStatus(`Model: ${model.id}`);
5086
+ this.showStatus(`Model: ${model.provider}/${model.id}`);
5036
5087
  this.checkDaxnutsEasterEgg(model);
5037
5088
  }
5038
5089
  catch (error) {
@@ -5341,22 +5392,29 @@ export class InteractiveMode {
5341
5392
  return;
5342
5393
  }
5343
5394
  }
5395
+ await this.refreshModelsDevProviderCatalog();
5396
+ const apiKeyProviders = this.getApiKeyLoginProviders(this.modelsDevProviderCatalog);
5344
5397
  this.showSelector((done) => {
5345
- const selector = new OAuthSelectorComponent(mode, this.session.modelRegistry.authStorage, async (providerId) => {
5398
+ const selector = new OAuthSelectorComponent(mode, this.session.modelRegistry.authStorage, async (provider) => {
5346
5399
  done();
5347
5400
  if (mode === "login") {
5348
- if (providerId === OPENROUTER_PROVIDER_ID) {
5349
- await this.handleOpenRouterApiKeyLogin();
5401
+ if (provider.kind === "api_key") {
5402
+ if (provider.id === OPENROUTER_PROVIDER_ID) {
5403
+ await this.handleOpenRouterApiKeyLogin();
5404
+ }
5405
+ else {
5406
+ await this.handleApiKeyLogin(provider.id, { providerName: provider.name });
5407
+ }
5350
5408
  }
5351
5409
  else {
5352
- await this.showLoginDialog(providerId);
5410
+ await this.showLoginDialog(provider.id);
5353
5411
  }
5354
5412
  }
5355
5413
  else {
5356
5414
  // Logout flow
5357
- const providerName = this.getProviderDisplayName(providerId);
5415
+ const providerName = this.getProviderDisplayName(provider.id);
5358
5416
  try {
5359
- this.session.modelRegistry.authStorage.logout(providerId);
5417
+ this.session.modelRegistry.authStorage.logout(provider.id);
5360
5418
  this.session.modelRegistry.refresh();
5361
5419
  await this.updateAvailableProviderCount();
5362
5420
  this.showStatus(`Logged out of ${providerName}`);
@@ -5368,21 +5426,156 @@ export class InteractiveMode {
5368
5426
  }, () => {
5369
5427
  done();
5370
5428
  this.ui.requestRender();
5371
- });
5429
+ }, apiKeyProviders);
5372
5430
  return { component: selector, focus: selector };
5373
5431
  });
5374
5432
  }
5433
+ async refreshModelsDevProviderCatalog() {
5434
+ if (this.modelsDevProviderCatalogRefreshPromise) {
5435
+ await this.modelsDevProviderCatalogRefreshPromise;
5436
+ return;
5437
+ }
5438
+ this.modelsDevProviderCatalogRefreshPromise = (async () => {
5439
+ const catalog = await loadModelsDevProviderCatalog();
5440
+ this.modelsDevProviderCatalogById = catalog;
5441
+ this.modelsDevProviderCatalog = Array.from(catalog.values())
5442
+ .map((provider) => ({
5443
+ id: provider.id,
5444
+ name: provider.name,
5445
+ env: provider.env,
5446
+ }))
5447
+ .sort((a, b) => a.name.localeCompare(b.name, "en") || a.id.localeCompare(b.id, "en"));
5448
+ })()
5449
+ .catch(() => {
5450
+ this.modelsDevProviderCatalog = MODELS_DEV_PROVIDERS;
5451
+ this.modelsDevProviderCatalogById = new Map(MODELS_DEV_PROVIDERS.map((provider) => [
5452
+ provider.id,
5453
+ {
5454
+ ...provider,
5455
+ models: [],
5456
+ },
5457
+ ]));
5458
+ })
5459
+ .finally(() => {
5460
+ this.modelsDevProviderCatalogRefreshPromise = undefined;
5461
+ });
5462
+ await this.modelsDevProviderCatalogRefreshPromise;
5463
+ }
5464
+ resolveModelsDevApi(modelNpm) {
5465
+ const npm = modelNpm?.toLowerCase() ?? "";
5466
+ if (npm.includes("anthropic"))
5467
+ return "anthropic-messages";
5468
+ if (npm.includes("google-vertex"))
5469
+ return "google-vertex";
5470
+ if (npm.includes("google"))
5471
+ return "google-generative-ai";
5472
+ if (npm.includes("amazon-bedrock"))
5473
+ return "bedrock-converse-stream";
5474
+ if (npm.includes("mistral"))
5475
+ return "mistral-conversations";
5476
+ if (npm.includes("@ai-sdk/openai") && !npm.includes("compatible"))
5477
+ return "openai-responses";
5478
+ return "openai-completions";
5479
+ }
5480
+ buildModelsDevProviderConfig(providerInfo) {
5481
+ const baseUrl = providerInfo.api ?? providerInfo.models.find((model) => !!model.api)?.api;
5482
+ if (!baseUrl)
5483
+ return undefined;
5484
+ if (providerInfo.models.length === 0)
5485
+ return undefined;
5486
+ const models = providerInfo.models.map((model) => ({
5487
+ id: model.id,
5488
+ name: model.name,
5489
+ api: this.resolveModelsDevApi(model.npm ?? providerInfo.npm),
5490
+ reasoning: model.reasoning,
5491
+ input: [...model.input],
5492
+ cost: model.cost,
5493
+ contextWindow: model.contextWindow,
5494
+ maxTokens: model.maxTokens,
5495
+ headers: Object.keys(model.headers).length > 0 ? model.headers : undefined,
5496
+ }));
5497
+ return {
5498
+ baseUrl,
5499
+ models,
5500
+ };
5501
+ }
5502
+ hasRegisteredProviderModels(providerId) {
5503
+ const registry = this.session.modelRegistry;
5504
+ if (typeof registry.getAll !== "function")
5505
+ return true;
5506
+ return registry.getAll().some((model) => model.provider === providerId);
5507
+ }
5508
+ async hydrateProviderModelsFromModelsDev(providerId) {
5509
+ if (this.hasRegisteredProviderModels(providerId))
5510
+ return true;
5511
+ await this.refreshModelsDevProviderCatalog();
5512
+ const providerInfo = this.modelsDevProviderCatalogById.get(providerId);
5513
+ if (!providerInfo)
5514
+ return false;
5515
+ const config = this.buildModelsDevProviderConfig(providerInfo);
5516
+ if (!config)
5517
+ return false;
5518
+ try {
5519
+ this.session.modelRegistry.registerProvider(providerId, config);
5520
+ return this.hasRegisteredProviderModels(providerId);
5521
+ }
5522
+ catch {
5523
+ return false;
5524
+ }
5525
+ }
5526
+ async hydrateMissingProviderModelsForSavedAuth() {
5527
+ const savedProviders = this.session.modelRegistry.authStorage.list();
5528
+ if (savedProviders.length === 0)
5529
+ return;
5530
+ for (const providerId of savedProviders) {
5531
+ if (this.hasRegisteredProviderModels(providerId))
5532
+ continue;
5533
+ await this.hydrateProviderModelsFromModelsDev(providerId);
5534
+ }
5535
+ }
5536
+ getApiKeyLoginProviders(modelsDevProviders) {
5537
+ const providerNames = new Map();
5538
+ this.apiKeyProviderDisplayNames.clear();
5539
+ for (const model of this.session.modelRegistry.getAll()) {
5540
+ if (!providerNames.has(model.provider)) {
5541
+ providerNames.set(model.provider, toProviderDisplayName(model.provider));
5542
+ }
5543
+ }
5544
+ for (const provider of modelsDevProviders) {
5545
+ const fallbackName = toProviderDisplayName(provider.id);
5546
+ const current = providerNames.get(provider.id);
5547
+ if (!current || current === fallbackName) {
5548
+ providerNames.set(provider.id, provider.name || fallbackName);
5549
+ }
5550
+ }
5551
+ for (const providerId of this.session.modelRegistry.authStorage.list()) {
5552
+ if (!providerNames.has(providerId)) {
5553
+ providerNames.set(providerId, toProviderDisplayName(providerId));
5554
+ }
5555
+ }
5556
+ const oauthProviderIds = new Set(this.session.modelRegistry.authStorage.getOAuthProviders().map((provider) => provider.id));
5557
+ const providers = [];
5558
+ for (const [id, name] of providerNames.entries()) {
5559
+ if (oauthProviderIds.has(id))
5560
+ continue;
5561
+ this.apiKeyProviderDisplayNames.set(id, name);
5562
+ providers.push({ id, name, kind: "api_key" });
5563
+ }
5564
+ providers.sort((a, b) => a.name.localeCompare(b.name));
5565
+ return providers;
5566
+ }
5375
5567
  getProviderDisplayName(providerId) {
5376
- if (providerId === OPENROUTER_PROVIDER_ID) {
5377
- return "OpenRouter";
5568
+ const apiKeyName = this.apiKeyProviderDisplayNames.get(providerId);
5569
+ if (apiKeyName) {
5570
+ return apiKeyName;
5378
5571
  }
5379
5572
  const providerInfo = this.session.modelRegistry.authStorage.getOAuthProviders().find((p) => p.id === providerId);
5380
- return providerInfo?.name || providerId;
5573
+ return providerInfo?.name || toProviderDisplayName(providerId);
5381
5574
  }
5382
- async handleOpenRouterApiKeyLogin(options) {
5575
+ async handleApiKeyLogin(providerId, options) {
5576
+ const providerName = options?.providerName || this.getProviderDisplayName(providerId);
5383
5577
  const openModelSelector = options?.openModelSelector ?? true;
5384
- const providerName = this.getProviderDisplayName(OPENROUTER_PROVIDER_ID);
5385
- const existingCredential = this.session.modelRegistry.authStorage.get(OPENROUTER_PROVIDER_ID);
5578
+ const existingCredential = this.session.modelRegistry.authStorage.get(providerId);
5386
5579
  if (existingCredential) {
5387
5580
  const overwrite = await this.showExtensionConfirm(`${providerName}: replace existing credentials?`, `Stored at ${getAuthPath()}`);
5388
5581
  if (!overwrite) {
@@ -5390,7 +5583,11 @@ export class InteractiveMode {
5390
5583
  return;
5391
5584
  }
5392
5585
  }
5393
- const keyInput = await this.showExtensionInput(`${providerName} API key\nCreate key: https://openrouter.ai/keys`, "sk-or-v1-...");
5586
+ const promptLines = [`${providerName} API key`];
5587
+ if (options?.createKeyUrl) {
5588
+ promptLines.push(`Create key: ${options.createKeyUrl}`);
5589
+ }
5590
+ const keyInput = await this.showExtensionInput(promptLines.join("\n"), options?.placeholder ?? "api-key");
5394
5591
  if (keyInput === undefined) {
5395
5592
  this.showStatus(`${providerName} login cancelled.`);
5396
5593
  return;
@@ -5400,13 +5597,28 @@ export class InteractiveMode {
5400
5597
  this.showWarning(`${providerName} API key cannot be empty.`);
5401
5598
  return;
5402
5599
  }
5403
- this.session.modelRegistry.authStorage.set(OPENROUTER_PROVIDER_ID, { type: "api_key", key: apiKey });
5600
+ this.session.modelRegistry.authStorage.set(providerId, { type: "api_key", key: apiKey });
5601
+ let hasProviderModels = this.hasRegisteredProviderModels(providerId);
5602
+ if (!hasProviderModels) {
5603
+ hasProviderModels = await this.hydrateProviderModelsFromModelsDev(providerId);
5604
+ }
5404
5605
  await this.updateAvailableProviderCount();
5405
5606
  this.showStatus(`${providerName} API key saved to ${getAuthPath()}`);
5406
- if (openModelSelector) {
5407
- await this.showModelProviderSelector(OPENROUTER_PROVIDER_ID);
5607
+ if (openModelSelector && hasProviderModels) {
5608
+ await this.showModelProviderSelector(providerId);
5609
+ }
5610
+ else if (openModelSelector) {
5611
+ this.showWarning(`${providerName} configured, but no models are available yet. Run /model after network is available.`);
5408
5612
  }
5409
5613
  }
5614
+ async handleOpenRouterApiKeyLogin(options) {
5615
+ await this.handleApiKeyLogin(OPENROUTER_PROVIDER_ID, {
5616
+ providerName: "OpenRouter",
5617
+ openModelSelector: options?.openModelSelector,
5618
+ createKeyUrl: "https://openrouter.ai/keys",
5619
+ placeholder: "sk-or-v1-...",
5620
+ });
5621
+ }
5410
5622
  async showLoginDialog(providerId) {
5411
5623
  const providerInfo = this.session.modelRegistry.authStorage.getOAuthProviders().find((p) => p.id === providerId);
5412
5624
  const providerName = this.getProviderDisplayName(providerId);