@openeryc/pi-coding-agent 0.75.55 → 0.75.56

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 (46) hide show
  1. package/CHANGELOG.md +12 -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/model-registry.d.ts +112 -0
  10. package/dist/core/model-registry.d.ts.map +1 -1
  11. package/dist/core/model-registry.js +99 -2
  12. package/dist/core/model-registry.js.map +1 -1
  13. package/dist/core/settings-manager.d.ts +4 -0
  14. package/dist/core/settings-manager.d.ts.map +1 -1
  15. package/dist/core/settings-manager.js +12 -0
  16. package/dist/core/settings-manager.js.map +1 -1
  17. package/dist/core/slash-commands.d.ts.map +1 -1
  18. package/dist/core/slash-commands.js +2 -2
  19. package/dist/core/slash-commands.js.map +1 -1
  20. package/dist/modes/interactive/components/index.d.ts +1 -1
  21. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  22. package/dist/modes/interactive/components/index.js +1 -1
  23. package/dist/modes/interactive/components/index.js.map +1 -1
  24. package/dist/modes/interactive/components/model-hub.d.ts +115 -0
  25. package/dist/modes/interactive/components/model-hub.d.ts.map +1 -0
  26. package/dist/modes/interactive/components/model-hub.js +753 -0
  27. package/dist/modes/interactive/components/model-hub.js.map +1 -0
  28. package/dist/modes/interactive/components/model-selector.d.ts +2 -44
  29. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  30. package/dist/modes/interactive/components/model-selector.js +2 -275
  31. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  32. package/dist/modes/interactive/interactive-mode.d.ts +4 -2
  33. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  34. package/dist/modes/interactive/interactive-mode.js +308 -132
  35. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  36. package/docs/models.md +20 -0
  37. package/docs/providers.md +13 -0
  38. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  39. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  40. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  41. package/examples/extensions/sandbox/package-lock.json +2 -2
  42. package/examples/extensions/sandbox/package.json +1 -1
  43. package/examples/extensions/with-deps/package-lock.json +2 -2
  44. package/examples/extensions/with-deps/package.json +1 -1
  45. package/npm-shrinkwrap.json +12 -12
  46. 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,60 @@ 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
+ // models.json apiKey is opaque (env/command/literal); do not surface the raw value
4007
+ existing = {
4008
+ providerId: options.providerId,
4009
+ baseUrl: listed.baseUrl ?? "",
4010
+ apiKey: "",
4011
+ models: listed.modelIds.map((id) => ({ id, name: id })),
4012
+ };
4013
+ }
4014
+ }
4015
+ const dialogProviderId = existing?.providerId ?? "custom-provider";
4016
+ const title = options.target === "models_json"
4017
+ ? isEdit
4018
+ ? `Edit models.json provider (${existing?.providerId})`
4019
+ : "Add custom provider (models.json)"
4020
+ : isEdit
4021
+ ? `Edit OpenAI Compatible (${existing?.providerId})`
4022
+ : "Configure OpenAI Compatible Provider";
4023
+ const dialog = new LoginDialogComponent(this.ui, dialogProviderId, (_success, _message) => { }, existing?.providerId ?? "Custom Provider", title);
3968
4024
  this.editorContainer.clear();
3969
4025
  this.editorContainer.addChild(dialog);
3970
4026
  this.ui.setFocus(dialog);
@@ -3976,45 +4032,165 @@ export class InteractiveMode {
3976
4032
  this.ui.requestRender();
3977
4033
  };
3978
4034
  try {
3979
- const baseUrl = (await dialog.showPrompt("Enter base URL:", "http://localhost:11434/v1")).trim();
4035
+ let providerId = existing?.providerId ?? "";
4036
+ if (!isEdit) {
4037
+ const rawName = (await dialog.showPrompt("Provider name/id (e.g. ollama, agnes):", existing?.providerId ?? "ollama")).trim();
4038
+ providerId = slugifyProviderId(rawName);
4039
+ if (!providerId) {
4040
+ throw new Error("Provider name cannot be empty.");
4041
+ }
4042
+ if (BUILT_IN_MODEL_PROVIDERS.has(providerId) && options.target === "openai_compatible") {
4043
+ throw new Error(`"${providerId}" is a built-in provider. Use a different name, or add it via Manage → models.json override.`);
4044
+ }
4045
+ }
4046
+ const baseUrl = (await dialog.showPrompt("Enter base URL:", existing?.baseUrl || "http://localhost:11434/v1")).trim();
3980
4047
  if (!baseUrl) {
3981
4048
  throw new Error("Base URL cannot be empty.");
3982
4049
  }
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);
4050
+ const apiKeyPrompt = options.target === "models_json"
4051
+ ? "API key (literal, env var name, or !command; blank = not-needed):"
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
+ // On models.json edit, blank keeps the existing apiKey field (not rewritten).
4055
+ const effectiveApiKey = apiKey
4056
+ ? apiKey
4057
+ : isEdit && options.target === "models_json"
4058
+ ? undefined
4059
+ : "not-needed";
4060
+ dialog.showInfo([theme.fg("muted", "Discovering models from /models ...")]);
4061
+ this.ui.requestRender();
4062
+ const discovered = await fetchOpenAICompatibleModels(baseUrl, effectiveApiKey);
4063
+ let models;
4064
+ if (discovered.ok && discovered.models.length > 0) {
4065
+ // Offer: use all discovered, or hand-edit
4066
+ const useAllLabel = `Use all ${discovered.models.length} discovered models`;
4067
+ const manualLabel = "Enter model ids manually";
4068
+ const preview = discovered.models
4069
+ .slice(0, 8)
4070
+ .map((m) => m.id)
4071
+ .join(", ");
4072
+ const more = discovered.models.length > 8 ? ` (+${discovered.models.length - 8} more)` : "";
4073
+ dialog.showInfo([
4074
+ theme.fg("success", `Found ${discovered.models.length} models`),
4075
+ theme.fg("muted", `${preview}${more}`),
4076
+ ]);
4077
+ this.ui.requestRender();
4078
+ const choice = await new Promise((resolve) => {
4079
+ const restoreDialog = () => {
4080
+ this.editorContainer.clear();
4081
+ this.editorContainer.addChild(dialog);
4082
+ this.ui.setFocus(dialog);
4083
+ this.ui.requestRender();
4084
+ };
4085
+ const selector = new ExtensionSelectorComponent("Discovered models", [useAllLabel, manualLabel], (option) => {
4086
+ restoreDialog();
4087
+ resolve(option);
4088
+ }, () => {
4089
+ restoreDialog();
4090
+ resolve(undefined);
4091
+ });
4092
+ this.editorContainer.clear();
4093
+ this.editorContainer.addChild(selector);
4094
+ this.ui.setFocus(selector);
4095
+ this.ui.requestRender();
4096
+ });
4097
+ if (!choice) {
4098
+ throw new Error("Login cancelled");
4099
+ }
4100
+ if (choice === useAllLabel) {
4101
+ models = discovered.models.map((m) => ({ id: m.id, name: m.name }));
4102
+ }
4103
+ else {
4104
+ const defaultIds = existing?.models.map((m) => m.id).join(", ") ||
4105
+ discovered.models
4106
+ .slice(0, 5)
4107
+ .map((m) => m.id)
4108
+ .join(", ");
4109
+ const modelNames = (await dialog.showPrompt("Model id(s), comma-separated:", defaultIds)).trim();
4110
+ if (!modelNames)
4111
+ throw new Error("At least one model name is required.");
4112
+ models = modelNames
4113
+ .split(",")
4114
+ .map((s) => s.trim())
4115
+ .filter((s) => s.length > 0)
4116
+ .map((id) => ({ id, name: id }));
4117
+ }
4118
+ }
4119
+ else {
4120
+ dialog.showInfo([
4121
+ theme.fg("warning", `Auto-discover failed: ${discovered.ok ? "no models" : discovered.error}`),
4122
+ theme.fg("muted", "Falling back to manual model entry."),
4123
+ ]);
4124
+ this.ui.requestRender();
4125
+ const defaultIds = existing?.models.map((m) => m.id).join(", ") || "llama3.1";
4126
+ const modelNames = (await dialog.showPrompt("Enter model name(s) (comma-separated):", defaultIds)).trim();
4127
+ if (!modelNames) {
4128
+ throw new Error("At least one model name is required.");
4129
+ }
4130
+ models = modelNames
4131
+ .split(",")
4132
+ .map((s) => s.trim())
4133
+ .filter((s) => s.length > 0)
4134
+ .map((id) => ({ id, name: id }));
4135
+ }
4136
+ if (options.target === "openai_compatible") {
4137
+ const storedKey = effectiveApiKey ?? "not-needed";
4138
+ // Replace previous id if renaming isn't supported (edit keeps id)
4139
+ if (isEdit && existing && existing.providerId !== providerId) {
4140
+ this.session.modelRegistry.authStorage.remove(existing.providerId);
4141
+ }
4142
+ this.session.modelRegistry.configureOpenAICompatibleProvider(providerId, {
4143
+ baseUrl,
4144
+ apiKey: storedKey,
4145
+ models,
4146
+ });
4147
+ this.session.modelRegistry.authStorage.set(providerId, {
4148
+ type: "openai_compatible",
4149
+ key: storedKey,
4150
+ baseUrl,
4151
+ models,
4152
+ });
4153
+ // refresh so getAvailable sees it
4154
+ this.session.modelRegistry.refresh();
4155
+ restoreEditor();
4156
+ await this.completeProviderAuthentication(providerId, baseUrl, "api_key", previousModel);
4157
+ }
4158
+ else {
4159
+ this.session.modelRegistry.upsertProviderInModelsJson(providerId, {
4160
+ baseUrl,
4161
+ ...(effectiveApiKey !== undefined ? { apiKey: effectiveApiKey } : {}),
4162
+ api: "openai-completions",
4163
+ models: models.map((m) => ({
4164
+ id: m.id,
4165
+ name: m.name,
4166
+ reasoning: false,
4167
+ input: ["text"],
4168
+ })),
4169
+ });
4170
+ restoreEditor();
4171
+ await this.updateAvailableProviderCount();
4172
+ // Prefer selecting first model if no real model yet
4173
+ if (isUnknownModel(previousModel)) {
4174
+ const available = this.session.modelRegistry.getAvailable().filter((m) => m.provider === providerId);
4175
+ if (available[0]) {
4176
+ try {
4177
+ await this.session.setModel(available[0]);
4178
+ this.footer.invalidate();
4179
+ this.updateEditorBorderColor();
4180
+ }
4181
+ catch {
4182
+ // ignore
4183
+ }
4184
+ }
4185
+ }
4186
+ this.showStatus(`Saved provider "${providerId}" to models.json (${models.length} model${models.length === 1 ? "" : "s"}). Comments/trailing commas in that file are not preserved.`);
4187
+ }
4012
4188
  }
4013
4189
  catch (error) {
4014
4190
  restoreEditor();
4015
4191
  const errorMsg = error instanceof Error ? error.message : String(error);
4016
4192
  if (errorMsg !== "Login cancelled") {
4017
- this.showError(`Failed to configure OpenAI Compatible provider: ${errorMsg}`);
4193
+ this.showError(`Failed to configure provider: ${errorMsg}`);
4018
4194
  }
4019
4195
  }
4020
4196
  }