@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.
- package/CHANGELOG.md +21 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +3 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/fetch-openai-models.d.ts +25 -0
- package/dist/core/fetch-openai-models.d.ts.map +1 -0
- package/dist/core/fetch-openai-models.js +107 -0
- package/dist/core/fetch-openai-models.js.map +1 -0
- package/dist/core/keybindings.d.ts +10 -0
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +8 -0
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/model-registry.d.ts +118 -0
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +137 -2
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/settings-manager.d.ts +4 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +12 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +2 -2
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +1 -1
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +1 -1
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/components/model-hub.d.ts +117 -0
- package/dist/modes/interactive/components/model-hub.d.ts.map +1 -0
- package/dist/modes/interactive/components/model-hub.js +775 -0
- package/dist/modes/interactive/components/model-hub.js.map +1 -0
- package/dist/modes/interactive/components/model-selector.d.ts +2 -44
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +2 -275
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +4 -2
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +310 -132
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/models.md +20 -0
- package/docs/providers.md +13 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +12 -12
- 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 {
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
3422
|
-
this.
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
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
|
|
3445
|
-
// Get all available models
|
|
3446
|
-
this.session.modelRegistry.refresh();
|
|
3433
|
+
async applyCycleEnabledIds(enabledIds) {
|
|
3447
3434
|
const allModels = this.session.modelRegistry.getAvailable();
|
|
3448
|
-
if (
|
|
3449
|
-
this.
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
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
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
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 (
|
|
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 =
|
|
3876
|
-
|
|
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
|
|
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
|
|
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
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
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
|
|
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
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
models
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
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
|
|
4195
|
+
this.showError(`Failed to configure provider: ${errorMsg}`);
|
|
4018
4196
|
}
|
|
4019
4197
|
}
|
|
4020
4198
|
}
|