@openeryc/pi-coding-agent 0.75.55 → 0.75.57

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 (50) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/core/agent-session.d.ts.map +1 -1
  3. package/dist/core/agent-session.js +3 -0
  4. package/dist/core/agent-session.js.map +1 -1
  5. package/dist/core/fetch-openai-models.d.ts +25 -0
  6. package/dist/core/fetch-openai-models.d.ts.map +1 -0
  7. package/dist/core/fetch-openai-models.js +107 -0
  8. package/dist/core/fetch-openai-models.js.map +1 -0
  9. package/dist/core/keybindings.d.ts +10 -0
  10. package/dist/core/keybindings.d.ts.map +1 -1
  11. package/dist/core/keybindings.js +8 -0
  12. package/dist/core/keybindings.js.map +1 -1
  13. package/dist/core/model-registry.d.ts +118 -0
  14. package/dist/core/model-registry.d.ts.map +1 -1
  15. package/dist/core/model-registry.js +137 -2
  16. package/dist/core/model-registry.js.map +1 -1
  17. package/dist/core/settings-manager.d.ts +4 -0
  18. package/dist/core/settings-manager.d.ts.map +1 -1
  19. package/dist/core/settings-manager.js +12 -0
  20. package/dist/core/settings-manager.js.map +1 -1
  21. package/dist/core/slash-commands.d.ts.map +1 -1
  22. package/dist/core/slash-commands.js +2 -2
  23. package/dist/core/slash-commands.js.map +1 -1
  24. package/dist/modes/interactive/components/index.d.ts +1 -1
  25. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  26. package/dist/modes/interactive/components/index.js +1 -1
  27. package/dist/modes/interactive/components/index.js.map +1 -1
  28. package/dist/modes/interactive/components/model-hub.d.ts +117 -0
  29. package/dist/modes/interactive/components/model-hub.d.ts.map +1 -0
  30. package/dist/modes/interactive/components/model-hub.js +775 -0
  31. package/dist/modes/interactive/components/model-hub.js.map +1 -0
  32. package/dist/modes/interactive/components/model-selector.d.ts +2 -44
  33. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  34. package/dist/modes/interactive/components/model-selector.js +2 -275
  35. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  36. package/dist/modes/interactive/interactive-mode.d.ts +4 -2
  37. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  38. package/dist/modes/interactive/interactive-mode.js +310 -132
  39. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  40. package/docs/models.md +20 -0
  41. package/docs/providers.md +13 -0
  42. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  43. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  44. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  45. package/examples/extensions/sandbox/package-lock.json +2 -2
  46. package/examples/extensions/sandbox/package.json +1 -1
  47. package/examples/extensions/with-deps/package-lock.json +2 -2
  48. package/examples/extensions/with-deps/package.json +1 -1
  49. package/npm-shrinkwrap.json +12 -12
  50. package/package.json +4 -4
@@ -12,6 +12,7 @@ import { spawn, spawnSync } from "child_process";
12
12
  import { APP_NAME, APP_TITLE, getAgentDir, getAuthPath, getDebugLogPath, getDocsPath, getSelfUpdateCommand, getShareViewerUrl, PACKAGE_NAME, VERSION, } from "../../config.js";
13
13
  import { parseSkillBlock } from "../../core/agent-session.js";
14
14
  import { SessionImportFileNotFoundError } from "../../core/agent-session-runtime.js";
15
+ import { fetchOpenAICompatibleModels, slugifyProviderId } from "../../core/fetch-openai-models.js";
15
16
  import { FooterDataProvider } from "../../core/footer-data-provider.js";
16
17
  import { configureHttpDispatcher, formatHttpIdleTimeoutMs } from "../../core/http-dispatcher.js";
17
18
  import { KeybindingsManager } from "../../core/keybindings.js";
@@ -52,9 +53,8 @@ import { FooterComponent } from "./components/footer.js";
52
53
  import { formatKeyText, keyDisplayText, keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js";
53
54
  import { LoginDialogComponent } from "./components/login-dialog.js";
54
55
  import { McpSelectorComponent } from "./components/mcp-selector.js";
55
- import { ModelSelectorComponent } from "./components/model-selector.js";
56
+ import { ModelHubComponent } from "./components/model-hub.js";
56
57
  import { OAuthSelectorComponent } from "./components/oauth-selector.js";
57
- import { ScopedModelsSelectorComponent } from "./components/scoped-models-selector.js";
58
58
  import { SessionSelectorComponent } from "./components/session-selector.js";
59
59
  import { SettingsSelectorComponent } from "./components/settings-selector.js";
60
60
  import { SkillInvocationMessageComponent } from "./components/skill-invocation-message.js";
@@ -1966,7 +1966,7 @@ export class InteractiveMode {
1966
1966
  this.defaultEditor.onAction("app.model.cycleBackward", () => this.cycleModel("backward"));
1967
1967
  // Global debug handler on TUI (works regardless of focus)
1968
1968
  this.ui.onDebug = () => this.handleDebugCommand();
1969
- this.defaultEditor.onAction("app.model.select", () => this.showModelSelector());
1969
+ this.defaultEditor.onAction("app.model.select", () => this.showModelHub("select"));
1970
1970
  this.defaultEditor.onAction("app.tools.expand", () => this.toggleToolOutputExpansion());
1971
1971
  this.defaultEditor.onAction("app.thinking.toggle", () => this.toggleThinkingBlockVisibility());
1972
1972
  this.defaultEditor.onAction("app.editor.external", () => this.openExternalEditor());
@@ -2014,7 +2014,7 @@ export class InteractiveMode {
2014
2014
  await fn();
2015
2015
  };
2016
2016
  this.commands.set("/settings", noArg(() => this.showSettingsSelector()));
2017
- this.commands.set("/scoped-models", noArg(() => this.showModelsSelector()));
2017
+ this.commands.set("/scoped-models", noArg(() => this.showModelHub("cycle")));
2018
2018
  this.commands.set("/model", async (text) => {
2019
2019
  const s = text.startsWith("/model ") ? text.slice(7).trim() : undefined;
2020
2020
  this.editor.setText("");
@@ -3348,7 +3348,7 @@ export class InteractiveMode {
3348
3348
  }
3349
3349
  async handleModelCommand(searchTerm) {
3350
3350
  if (!searchTerm) {
3351
- this.showModelSelector();
3351
+ this.showModelHub("select");
3352
3352
  return;
3353
3353
  }
3354
3354
  const model = await this.findExactModelMatch(searchTerm);
@@ -3366,7 +3366,7 @@ export class InteractiveMode {
3366
3366
  }
3367
3367
  return;
3368
3368
  }
3369
- this.showModelSelector(searchTerm);
3369
+ this.showModelHub("select", searchTerm);
3370
3370
  }
3371
3371
  async findExactModelMatch(searchTerm) {
3372
3372
  const models = await this.getModelCandidates();
@@ -3418,93 +3418,100 @@ export class InteractiveMode {
3418
3418
  // Ignore auth lookup failures for warning-only checks.
3419
3419
  }
3420
3420
  }
3421
- showModelSelector(initialSearchInput) {
3422
- this.showSelector((done) => {
3423
- const selector = new ModelSelectorComponent(this.ui, this.session.model, this.settingsManager, this.session.modelRegistry, this.session.scopedModels, async (model) => {
3424
- try {
3425
- await this.session.setModel(model);
3426
- this.footer.invalidate();
3427
- this.updateEditorBorderColor();
3428
- done();
3429
- this.showStatus(`Model: ${model.id}`);
3430
- void this.maybeWarnAboutAnthropicSubscriptionAuth(model);
3431
- this.checkDaxnutsEasterEgg(model);
3432
- }
3433
- catch (error) {
3434
- done();
3435
- this.showError(error instanceof Error ? error.message : String(error));
3436
- }
3437
- }, () => {
3438
- done();
3439
- this.ui.requestRender();
3440
- }, initialSearchInput);
3441
- return { component: selector, focus: selector };
3442
- });
3421
+ async resolveEnabledModelIds() {
3422
+ const sessionScopedModels = this.session.scopedModels;
3423
+ if (sessionScopedModels.length > 0) {
3424
+ return sessionScopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
3425
+ }
3426
+ const patterns = this.settingsManager.getEnabledModels();
3427
+ if (patterns !== undefined && patterns.length > 0) {
3428
+ const scopedModels = await resolveModelScope(patterns, this.session.modelRegistry);
3429
+ return scopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
3430
+ }
3431
+ return null;
3443
3432
  }
3444
- async showModelsSelector() {
3445
- // Get all available models
3446
- this.session.modelRegistry.refresh();
3433
+ async applyCycleEnabledIds(enabledIds) {
3447
3434
  const allModels = this.session.modelRegistry.getAvailable();
3448
- if (allModels.length === 0) {
3449
- this.showStatus("No models available");
3450
- return;
3451
- }
3452
- // Check if session has scoped models (from previous session-only changes or CLI --models)
3453
- const sessionScopedModels = this.session.scopedModels;
3454
- const hasSessionScope = sessionScopedModels.length > 0;
3455
- // Build enabled model IDs from session state or settings
3456
- let currentEnabledIds = null;
3457
- if (hasSessionScope) {
3458
- // Use current session's scoped models
3459
- currentEnabledIds = sessionScopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
3435
+ if (enabledIds && enabledIds.length > 0 && enabledIds.length < allModels.length) {
3436
+ const newScopedModels = await resolveModelScope(enabledIds, this.session.modelRegistry);
3437
+ this.session.setScopedModels(newScopedModels.map((sm) => ({
3438
+ model: sm.model,
3439
+ thinkingLevel: sm.thinkingLevel,
3440
+ })));
3460
3441
  }
3461
3442
  else {
3462
- // Fall back to settings
3463
- const patterns = this.settingsManager.getEnabledModels();
3464
- if (patterns !== undefined && patterns.length > 0) {
3465
- const scopedModels = await resolveModelScope(patterns, this.session.modelRegistry);
3466
- currentEnabledIds = scopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
3467
- }
3468
- }
3469
- // Helper to update session's scoped models (session-only, no persist)
3470
- const updateSessionModels = async (enabledIds) => {
3471
- currentEnabledIds = enabledIds === null ? null : [...enabledIds];
3472
- if (enabledIds && enabledIds.length > 0 && enabledIds.length < allModels.length) {
3473
- const newScopedModels = await resolveModelScope(enabledIds, this.session.modelRegistry);
3474
- this.session.setScopedModels(newScopedModels.map((sm) => ({
3475
- model: sm.model,
3476
- thinkingLevel: sm.thinkingLevel,
3477
- })));
3478
- }
3479
- else {
3480
- // All enabled or none enabled = no filter
3481
- this.session.setScopedModels([]);
3482
- }
3483
- await this.updateAvailableProviderCount();
3484
- this.ui.requestRender();
3485
- };
3486
- this.showSelector((done) => {
3487
- const selector = new ScopedModelsSelectorComponent({
3488
- allModels,
3489
- enabledModelIds: currentEnabledIds,
3490
- }, {
3491
- onChange: async (enabledIds) => {
3492
- await updateSessionModels(enabledIds);
3493
- },
3494
- onPersist: (enabledIds) => {
3495
- // Persist to settings
3496
- const newPatterns = enabledIds === null || enabledIds.length === allModels.length
3497
- ? undefined // All enabled = clear filter
3498
- : enabledIds;
3499
- this.settingsManager.setEnabledModels(newPatterns ? [...newPatterns] : undefined);
3500
- this.showStatus("Model selection saved to settings");
3501
- },
3502
- onCancel: () => {
3503
- done();
3504
- this.ui.requestRender();
3505
- },
3443
+ this.session.setScopedModels([]);
3444
+ }
3445
+ await this.updateAvailableProviderCount();
3446
+ this.ui.requestRender();
3447
+ }
3448
+ showModelHub(initialTab = "select", initialSearchInput) {
3449
+ void this.resolveEnabledModelIds().then((enabledModelIds) => {
3450
+ this.showSelector((done) => {
3451
+ const hub = new ModelHubComponent(this.ui, this.session.model, this.settingsManager, this.session.modelRegistry, this.session.scopedModels, enabledModelIds, {
3452
+ onSelectModel: async (model) => {
3453
+ try {
3454
+ await this.session.setModel(model);
3455
+ this.footer.invalidate();
3456
+ this.updateEditorBorderColor();
3457
+ done();
3458
+ this.showStatus(`Model: ${model.id}`);
3459
+ void this.maybeWarnAboutAnthropicSubscriptionAuth(model);
3460
+ this.checkDaxnutsEasterEgg(model);
3461
+ }
3462
+ catch (error) {
3463
+ done();
3464
+ this.showError(error instanceof Error ? error.message : String(error));
3465
+ }
3466
+ },
3467
+ onCancel: () => {
3468
+ done();
3469
+ this.ui.requestRender();
3470
+ },
3471
+ onCycleChange: async (ids) => {
3472
+ await this.applyCycleEnabledIds(ids);
3473
+ },
3474
+ onCyclePersist: async (ids) => {
3475
+ const allModels = this.session.modelRegistry.getAvailable();
3476
+ const newPatterns = ids === null || ids.length === allModels.length ? undefined : ids;
3477
+ this.settingsManager.setEnabledModels(newPatterns ? [...newPatterns] : undefined);
3478
+ this.showStatus("Model selection saved to settings");
3479
+ },
3480
+ onManageAdd: async () => {
3481
+ done();
3482
+ await this.showCustomProviderWizard({ mode: "add", target: "models_json" });
3483
+ },
3484
+ onManageEdit: async (source, providerId) => {
3485
+ done();
3486
+ await this.showCustomProviderWizard({ mode: "edit", target: source, providerId });
3487
+ },
3488
+ onManageRemove: async (source, providerId) => {
3489
+ done();
3490
+ const confirmed = await this.showExtensionConfirm("Remove provider", `Remove "${providerId}" from ${source === "models_json" ? "models.json" : "auth.json"}?`);
3491
+ if (!confirmed) {
3492
+ this.showModelHub("manage");
3493
+ return;
3494
+ }
3495
+ try {
3496
+ if (source === "models_json") {
3497
+ this.session.modelRegistry.removeProviderFromModelsJson(providerId);
3498
+ }
3499
+ else {
3500
+ this.session.modelRegistry.authStorage.remove(providerId);
3501
+ this.session.modelRegistry.refresh();
3502
+ }
3503
+ await this.updateAvailableProviderCount();
3504
+ this.showStatus(`Removed provider ${providerId}`);
3505
+ this.showModelHub("manage");
3506
+ }
3507
+ catch (error) {
3508
+ this.showError(error instanceof Error ? error.message : String(error));
3509
+ this.showModelHub("manage");
3510
+ }
3511
+ },
3512
+ }, { initialTab, initialSearchInput });
3513
+ return { component: hub, focus: hub };
3506
3514
  });
3507
- return { component: selector, focus: selector };
3508
3515
  });
3509
3516
  }
3510
3517
  showUserMessageSelector() {
@@ -3865,17 +3872,18 @@ export class InteractiveMode {
3865
3872
  if (isUnknownModel(previousModel)) {
3866
3873
  const availableModels = this.session.modelRegistry.getAvailable();
3867
3874
  const providerModels = availableModels.filter((model) => model.provider === providerId);
3868
- if (!hasDefaultModelProvider(providerId)) {
3869
- selectionError = `${actionLabel}, but no default model is configured for provider "${providerId}". Use /model to select a model.`;
3870
- }
3871
- else if (providerModels.length === 0) {
3875
+ if (providerModels.length === 0) {
3872
3876
  selectionError = `${actionLabel}, but no models are available for that provider. Use /model to select a model.`;
3873
3877
  }
3874
3878
  else {
3875
- const defaultModelId = defaultModelPerProvider[providerId];
3876
- selectedModel = providerModels.find((model) => model.id === defaultModelId);
3879
+ const defaultModelId = hasDefaultModelProvider(providerId)
3880
+ ? defaultModelPerProvider[providerId]
3881
+ : undefined;
3882
+ selectedModel =
3883
+ (defaultModelId ? providerModels.find((model) => model.id === defaultModelId) : undefined) ??
3884
+ providerModels[0];
3877
3885
  if (!selectedModel) {
3878
- selectionError = `${actionLabel}, but its default model "${defaultModelId}" is not available. Use /model to select a model.`;
3886
+ selectionError = `${actionLabel}, but no model could be selected. Use /model to select a model.`;
3879
3887
  }
3880
3888
  else {
3881
3889
  try {
@@ -3884,7 +3892,7 @@ export class InteractiveMode {
3884
3892
  catch (error) {
3885
3893
  selectedModel = undefined;
3886
3894
  const errorMessage = error instanceof Error ? error.message : String(error);
3887
- selectionError = `${actionLabel}, but selecting its default model failed: ${errorMessage}. Use /model to select a model.`;
3895
+ selectionError = `${actionLabel}, but selecting model failed: ${errorMessage}. Use /model to select a model.`;
3888
3896
  }
3889
3897
  }
3890
3898
  }
@@ -3959,12 +3967,59 @@ export class InteractiveMode {
3959
3967
  }
3960
3968
  }
3961
3969
  async showOpenAICompatibleLoginDialog() {
3970
+ await this.showCustomProviderWizard({ mode: "add", target: "openai_compatible" });
3971
+ }
3972
+ /**
3973
+ * Interactive wizard to add/edit a custom OpenAI-compatible provider.
3974
+ * - target models_json: writes ~/.pi/agent/models.json (Manage tab default)
3975
+ * - target openai_compatible: writes auth.json ( /login path )
3976
+ */
3977
+ async showCustomProviderWizard(options) {
3962
3978
  const previousModel = this.session.model;
3963
- const providerId = `openai-compatible-${crypto.randomUUID().slice(0, 8)}`;
3964
- const providerName = "OpenAI Compatible";
3965
- const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {
3966
- // Completion handled below
3967
- }, providerName, "Configure OpenAI Compatible Provider");
3979
+ const isEdit = options.mode === "edit" && !!options.providerId;
3980
+ let existing;
3981
+ if (isEdit && options.providerId) {
3982
+ if (options.target === "openai_compatible") {
3983
+ const all = this.session.modelRegistry.authStorage.getOpenAICompatibleProviders();
3984
+ const info = all[options.providerId];
3985
+ if (!info) {
3986
+ this.showError(`Provider "${options.providerId}" not found in auth.json`);
3987
+ this.showModelHub("manage");
3988
+ return;
3989
+ }
3990
+ existing = {
3991
+ providerId: options.providerId,
3992
+ baseUrl: info.baseUrl,
3993
+ apiKey: info.key,
3994
+ models: info.models,
3995
+ };
3996
+ }
3997
+ else {
3998
+ const listed = this.session.modelRegistry
3999
+ .listModelsJsonProviders()
4000
+ .find((p) => p.name === options.providerId);
4001
+ if (!listed) {
4002
+ this.showError(`Provider "${options.providerId}" not found in models.json`);
4003
+ this.showModelHub("manage");
4004
+ return;
4005
+ }
4006
+ existing = {
4007
+ providerId: options.providerId,
4008
+ baseUrl: listed.baseUrl ?? "",
4009
+ apiKey: listed.apiKey ?? "",
4010
+ models: listed.modelIds.map((id) => ({ id, name: id })),
4011
+ };
4012
+ }
4013
+ }
4014
+ const dialogProviderId = existing?.providerId ?? "custom-provider";
4015
+ const title = options.target === "models_json"
4016
+ ? isEdit
4017
+ ? `Edit models.json provider (${existing?.providerId})`
4018
+ : "Add custom provider (models.json)"
4019
+ : isEdit
4020
+ ? `Edit OpenAI Compatible (${existing?.providerId})`
4021
+ : "Configure OpenAI Compatible Provider";
4022
+ const dialog = new LoginDialogComponent(this.ui, dialogProviderId, (_success, _message) => { }, existing?.providerId ?? "Custom Provider", title);
3968
4023
  this.editorContainer.clear();
3969
4024
  this.editorContainer.addChild(dialog);
3970
4025
  this.ui.setFocus(dialog);
@@ -3976,45 +4031,168 @@ export class InteractiveMode {
3976
4031
  this.ui.requestRender();
3977
4032
  };
3978
4033
  try {
3979
- const baseUrl = (await dialog.showPrompt("Enter base URL:", "http://localhost:11434/v1")).trim();
4034
+ const rawName = (await dialog.showPrompt(isEdit ? "Provider name/id:" : "Provider name/id (e.g. ollama, agnes):", existing?.providerId ?? "ollama")).trim();
4035
+ const providerId = slugifyProviderId(rawName);
4036
+ if (!providerId) {
4037
+ throw new Error("Provider name cannot be empty.");
4038
+ }
4039
+ if (BUILT_IN_MODEL_PROVIDERS.has(providerId) && options.target === "openai_compatible") {
4040
+ throw new Error(`"${providerId}" is a built-in provider. Use a different name, or add it via Manage → models.json override.`);
4041
+ }
4042
+ const baseUrl = (await dialog.showPrompt("Enter base URL:", existing?.baseUrl || "http://localhost:11434/v1")).trim();
3980
4043
  if (!baseUrl) {
3981
4044
  throw new Error("Base URL cannot be empty.");
3982
4045
  }
3983
- const apiKey = (await dialog.showPrompt("Enter API key (or leave blank for local endpoints):")).trim();
3984
- const modelNames = (await dialog.showPrompt("Enter model name(s) (comma-separated):", "llama3.1")).trim();
3985
- if (!modelNames) {
3986
- throw new Error("At least one model name is required.");
3987
- }
3988
- const models = modelNames
3989
- .split(",")
3990
- .map((s) => s.trim())
3991
- .filter((s) => s.length > 0)
3992
- .map((id) => ({
3993
- id,
3994
- name: id,
3995
- }));
3996
- const effectiveApiKey = apiKey || "not-needed";
3997
- // Immediately register the provider so models are available
3998
- this.session.modelRegistry.configureOpenAICompatibleProvider(providerId, {
3999
- baseUrl,
4000
- apiKey: effectiveApiKey,
4001
- models,
4002
- });
4003
- // Persist to auth.json so it survives restarts
4004
- this.session.modelRegistry.authStorage.set(providerId, {
4005
- type: "openai_compatible",
4006
- key: effectiveApiKey,
4007
- baseUrl,
4008
- models,
4009
- });
4010
- restoreEditor();
4011
- await this.completeProviderAuthentication(providerId, `${baseUrl}`, "api_key", previousModel);
4046
+ const apiKeyPrompt = options.target === "models_json"
4047
+ ? isEdit
4048
+ ? "API key (literal/env/!command; blank keeps current):"
4049
+ : "API key (literal, env var name, or !command; blank = not-needed):"
4050
+ : isEdit
4051
+ ? "API key (blank keeps current; type not-needed for local):"
4052
+ : "Enter API key (or leave blank for local endpoints):";
4053
+ const apiKey = (await dialog.showPrompt(apiKeyPrompt, existing?.apiKey === "not-needed" ? "" : (existing?.apiKey ?? ""))).trim();
4054
+ // Blank on edit keeps the existing apiKey (models.json and openai_compatible).
4055
+ const effectiveApiKey = apiKey ? apiKey : isEdit ? undefined : "not-needed";
4056
+ dialog.showInfo([theme.fg("muted", "Discovering models from /models ...")]);
4057
+ this.ui.requestRender();
4058
+ const discovered = await fetchOpenAICompatibleModels(baseUrl, effectiveApiKey);
4059
+ let models;
4060
+ if (discovered.ok && discovered.models.length > 0) {
4061
+ // Offer: use all discovered, or hand-edit
4062
+ const useAllLabel = `Use all ${discovered.models.length} discovered models`;
4063
+ const manualLabel = "Enter model ids manually";
4064
+ const preview = discovered.models
4065
+ .slice(0, 8)
4066
+ .map((m) => m.id)
4067
+ .join(", ");
4068
+ const more = discovered.models.length > 8 ? ` (+${discovered.models.length - 8} more)` : "";
4069
+ dialog.showInfo([
4070
+ theme.fg("success", `Found ${discovered.models.length} models`),
4071
+ theme.fg("muted", `${preview}${more}`),
4072
+ ]);
4073
+ this.ui.requestRender();
4074
+ const choice = await new Promise((resolve) => {
4075
+ const restoreDialog = () => {
4076
+ this.editorContainer.clear();
4077
+ this.editorContainer.addChild(dialog);
4078
+ this.ui.setFocus(dialog);
4079
+ this.ui.requestRender();
4080
+ };
4081
+ const selector = new ExtensionSelectorComponent("Discovered models", [useAllLabel, manualLabel], (option) => {
4082
+ restoreDialog();
4083
+ resolve(option);
4084
+ }, () => {
4085
+ restoreDialog();
4086
+ resolve(undefined);
4087
+ });
4088
+ this.editorContainer.clear();
4089
+ this.editorContainer.addChild(selector);
4090
+ this.ui.setFocus(selector);
4091
+ this.ui.requestRender();
4092
+ });
4093
+ if (!choice) {
4094
+ throw new Error("Login cancelled");
4095
+ }
4096
+ if (choice === useAllLabel) {
4097
+ models = discovered.models.map((m) => ({ id: m.id, name: m.name }));
4098
+ }
4099
+ else {
4100
+ const defaultIds = existing?.models.map((m) => m.id).join(", ") ||
4101
+ discovered.models
4102
+ .slice(0, 5)
4103
+ .map((m) => m.id)
4104
+ .join(", ");
4105
+ const modelNames = (await dialog.showPrompt("Model id(s), comma-separated:", defaultIds)).trim();
4106
+ if (!modelNames)
4107
+ throw new Error("At least one model name is required.");
4108
+ models = modelNames
4109
+ .split(",")
4110
+ .map((s) => s.trim())
4111
+ .filter((s) => s.length > 0)
4112
+ .map((id) => ({ id, name: id }));
4113
+ }
4114
+ }
4115
+ else {
4116
+ dialog.showInfo([
4117
+ theme.fg("warning", `Auto-discover failed: ${discovered.ok ? "no models" : discovered.error}`),
4118
+ theme.fg("muted", "Falling back to manual model entry."),
4119
+ ]);
4120
+ this.ui.requestRender();
4121
+ const defaultIds = existing?.models.map((m) => m.id).join(", ") || "llama3.1";
4122
+ const modelNames = (await dialog.showPrompt("Enter model name(s) (comma-separated):", defaultIds)).trim();
4123
+ if (!modelNames) {
4124
+ throw new Error("At least one model name is required.");
4125
+ }
4126
+ models = modelNames
4127
+ .split(",")
4128
+ .map((s) => s.trim())
4129
+ .filter((s) => s.length > 0)
4130
+ .map((id) => ({ id, name: id }));
4131
+ }
4132
+ if (options.target === "openai_compatible") {
4133
+ const storedKey = effectiveApiKey ?? (isEdit && existing?.apiKey ? existing.apiKey : "not-needed");
4134
+ // Rename: drop old id if the name changed
4135
+ if (isEdit && existing && existing.providerId !== providerId) {
4136
+ this.session.modelRegistry.authStorage.remove(existing.providerId);
4137
+ }
4138
+ this.session.modelRegistry.configureOpenAICompatibleProvider(providerId, {
4139
+ baseUrl,
4140
+ apiKey: storedKey,
4141
+ models,
4142
+ });
4143
+ this.session.modelRegistry.authStorage.set(providerId, {
4144
+ type: "openai_compatible",
4145
+ key: storedKey,
4146
+ baseUrl,
4147
+ models,
4148
+ });
4149
+ // refresh so getAvailable sees it
4150
+ this.session.modelRegistry.refresh();
4151
+ restoreEditor();
4152
+ await this.completeProviderAuthentication(providerId, baseUrl, "api_key", previousModel);
4153
+ }
4154
+ else {
4155
+ const writeConfig = {
4156
+ baseUrl,
4157
+ ...(effectiveApiKey !== undefined ? { apiKey: effectiveApiKey } : {}),
4158
+ api: "openai-completions",
4159
+ models: models.map((m) => ({
4160
+ id: m.id,
4161
+ name: m.name,
4162
+ reasoning: false,
4163
+ input: ["text"],
4164
+ })),
4165
+ };
4166
+ if (isEdit && existing && existing.providerId !== providerId) {
4167
+ this.session.modelRegistry.renameProviderInModelsJson(existing.providerId, providerId, writeConfig);
4168
+ }
4169
+ else {
4170
+ this.session.modelRegistry.upsertProviderInModelsJson(providerId, writeConfig);
4171
+ }
4172
+ restoreEditor();
4173
+ await this.updateAvailableProviderCount();
4174
+ // Prefer selecting first model if no real model yet
4175
+ if (isUnknownModel(previousModel)) {
4176
+ const available = this.session.modelRegistry.getAvailable().filter((m) => m.provider === providerId);
4177
+ if (available[0]) {
4178
+ try {
4179
+ await this.session.setModel(available[0]);
4180
+ this.footer.invalidate();
4181
+ this.updateEditorBorderColor();
4182
+ }
4183
+ catch {
4184
+ // ignore
4185
+ }
4186
+ }
4187
+ }
4188
+ this.showStatus(`Saved provider "${providerId}" to models.json (${models.length} model${models.length === 1 ? "" : "s"}). Comments/trailing commas in that file are not preserved.`);
4189
+ }
4012
4190
  }
4013
4191
  catch (error) {
4014
4192
  restoreEditor();
4015
4193
  const errorMsg = error instanceof Error ? error.message : String(error);
4016
4194
  if (errorMsg !== "Login cancelled") {
4017
- this.showError(`Failed to configure OpenAI Compatible provider: ${errorMsg}`);
4195
+ this.showError(`Failed to configure provider: ${errorMsg}`);
4018
4196
  }
4019
4197
  }
4020
4198
  }