@wuyaos/pi-model-config 0.1.5 → 0.1.7
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/model-config.schema.json +8 -0
- package/package.json +1 -1
- package/src/command.ts +89 -69
- package/src/config.ts +1 -0
- package/src/i18n.ts +34 -22
- package/src/image-apis/common.ts +2 -2
- package/src/panel.ts +27 -2
- package/src/runtime.ts +52 -22
- package/src/selector.ts +22 -4
- package/src/types.ts +19 -2
package/model-config.schema.json
CHANGED
|
@@ -192,6 +192,14 @@
|
|
|
192
192
|
},
|
|
193
193
|
"uniqueItems": true
|
|
194
194
|
},
|
|
195
|
+
"hiddenRemoteIds": {
|
|
196
|
+
"description": "Remote catalog model IDs hidden by the user across discovery refreshes.",
|
|
197
|
+
"type": "array",
|
|
198
|
+
"items": {
|
|
199
|
+
"type": "string"
|
|
200
|
+
},
|
|
201
|
+
"uniqueItems": true
|
|
202
|
+
},
|
|
195
203
|
"models": {
|
|
196
204
|
"type": "array",
|
|
197
205
|
"items": {
|
package/package.json
CHANGED
package/src/command.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
showPreviewPanel,
|
|
19
19
|
showProviderPanel,
|
|
20
20
|
showConfirmationPanel,
|
|
21
|
+
showRestoreHiddenPanel,
|
|
21
22
|
type MenuPanelState,
|
|
22
23
|
type OverviewPanelState,
|
|
23
24
|
type PanelItem,
|
|
@@ -36,7 +37,7 @@ import type {
|
|
|
36
37
|
ProviderConnectionConfig,
|
|
37
38
|
ProviderDraft,
|
|
38
39
|
} from "./types.ts";
|
|
39
|
-
import { identityOf } from "./types.ts";
|
|
40
|
+
import { hiddenRemoteKey, identityOf, isHiddenRemote, normalizeHiddenRemoteKeys } from "./types.ts";
|
|
40
41
|
import { loadUiLanguage, saveUiLanguage } from "./ui-config.ts";
|
|
41
42
|
import { secretInput, withCancellableLoader } from "./ui.ts";
|
|
42
43
|
import { showModelTestFlow } from "./model-test.ts";
|
|
@@ -157,6 +158,7 @@ function draftFrom(providerId: string, provider: ModelsJsonProvider | undefined,
|
|
|
157
158
|
classifierApi: config?.classifierApi ?? "typesafe-system-one",
|
|
158
159
|
imageApi: config?.imageApi ?? "openrouter-images",
|
|
159
160
|
defaultType: config?.defaultType ?? "chat",
|
|
161
|
+
hiddenRemoteIds: normalizeHiddenRemoteKeys(config?.hiddenRemoteIds ?? []),
|
|
160
162
|
};
|
|
161
163
|
}
|
|
162
164
|
|
|
@@ -248,9 +250,9 @@ function connectionPanelFields(language: UiLanguage, draft: ProviderDraft): Pane
|
|
|
248
250
|
];
|
|
249
251
|
}
|
|
250
252
|
|
|
251
|
-
async function editConnectionPanel(ctx: ExtensionCommandContext, language: UiLanguage, draft: ProviderDraft, state: MenuPanelState, canTakeover: boolean): Promise<"models" | "takeover" | "testModels" | "preview" | "back"> {
|
|
253
|
+
async function editConnectionPanel(ctx: ExtensionCommandContext, language: UiLanguage, draft: ProviderDraft, state: MenuPanelState, canTakeover: boolean, hiddenRemoteCount: number): Promise<"models" | "takeover" | "restoreHidden" | "testModels" | "preview" | "back"> {
|
|
252
254
|
while (true) {
|
|
253
|
-
const result = await showConnectionPanel(ctx, language, draft.id, connectionPanelFields(language, draft), state, canTakeover);
|
|
255
|
+
const result = await showConnectionPanel(ctx, language, draft.id, connectionPanelFields(language, draft), state, canTakeover, hiddenRemoteCount);
|
|
254
256
|
if (result.action !== "field") return result.action;
|
|
255
257
|
try {
|
|
256
258
|
switch (result.fieldId) {
|
|
@@ -409,7 +411,9 @@ export function applyChanges(
|
|
|
409
411
|
const originalRows = new Map(
|
|
410
412
|
(oldProvider.models ?? []).filter((row) => typeof row.id === "string").map((row) => [row.id as string, row]),
|
|
411
413
|
);
|
|
412
|
-
|
|
414
|
+
// models.json rows are managed scope by default: selected rows are rewritten (merging
|
|
415
|
+
// unknown fields), unchecked rows are removed. Only rows without a string id are kept verbatim.
|
|
416
|
+
const managedScopeIds = new Set([...oldManagedIds, ...adoptedChatIds, ...originalRows.keys()]);
|
|
413
417
|
const unmanagedRows = (oldProvider.models ?? []).filter((row) => typeof row.id !== "string" || !managedScopeIds.has(row.id));
|
|
414
418
|
const selectedChat = selected.filter((model) => model.type === "chat");
|
|
415
419
|
provider.models = [...unmanagedRows, ...selectedChat.map((model) => ({
|
|
@@ -422,8 +426,10 @@ export function applyChanges(
|
|
|
422
426
|
if (draft.authMode === "models-json") provider.apiKey = draft.apiKeyRaw; else delete provider.apiKey;
|
|
423
427
|
models.providers[providerId] = provider;
|
|
424
428
|
|
|
429
|
+
const normalizedHiddenRemoteIds = normalizeHiddenRemoteKeys(draft.hiddenRemoteIds ?? []);
|
|
430
|
+
const { hiddenRemoteIds: _oldHiddenRemoteIds, ...oldConfigWithoutHiddenRemoteIds } = oldConfig ?? {};
|
|
425
431
|
config.providers[providerId] = {
|
|
426
|
-
...
|
|
432
|
+
...oldConfigWithoutHiddenRemoteIds,
|
|
427
433
|
name: draft.name,
|
|
428
434
|
owned: draft.owned,
|
|
429
435
|
enabled: draft.enabled,
|
|
@@ -434,6 +440,7 @@ export function applyChanges(
|
|
|
434
440
|
imageApi: draft.imageApi,
|
|
435
441
|
defaultType: draft.defaultType,
|
|
436
442
|
managedChatIds: selectedChat.map((model) => model.id),
|
|
443
|
+
...(normalizedHiddenRemoteIds.length ? { hiddenRemoteIds: normalizedHiddenRemoteIds } : {}),
|
|
437
444
|
models: selected.filter((model): model is Extract<ManagedModel, { type: "classifier" | "image" }> => model.type !== "chat"),
|
|
438
445
|
modelMetadata: modelMetadata.map((entry) => structuredClone(entry)),
|
|
439
446
|
};
|
|
@@ -512,6 +519,7 @@ export interface ProviderChoice {
|
|
|
512
519
|
id: string;
|
|
513
520
|
label: string;
|
|
514
521
|
priority: number;
|
|
522
|
+
enabled: boolean;
|
|
515
523
|
}
|
|
516
524
|
|
|
517
525
|
export function buildConfiguredProviderChoices(
|
|
@@ -522,20 +530,21 @@ export function buildConfiguredProviderChoices(
|
|
|
522
530
|
): ProviderChoice[] {
|
|
523
531
|
const ids = new Set([...Object.keys(models.providers), ...Object.keys(config.providers)]);
|
|
524
532
|
const choices: ProviderChoice[] = [];
|
|
525
|
-
for (const id of ids) {
|
|
533
|
+
for (const id of [...ids].sort((a, b) => a.localeCompare(b))) {
|
|
526
534
|
const owned = config.providers[id]?.owned === true;
|
|
527
535
|
const builtin = builtinProviderIds.has(id);
|
|
528
536
|
const name = config.providers[id]?.name ?? models.providers[id]?.name;
|
|
529
537
|
const displayBase = name && name !== id ? `${name} (${id})` : id;
|
|
530
538
|
const display = config.providers[id]?.enabled === false ? `${displayBase} · ${t(language, "providerDisabled")}` : displayBase;
|
|
531
539
|
const status = t(language, builtin ? "providerTagBuiltin" : "providerTagCustom");
|
|
540
|
+
const enabled = config.providers[id]?.enabled !== false;
|
|
532
541
|
choices.push(owned
|
|
533
|
-
? { id, label: t(language, "providerLabelCustom", { display, status }), priority: 0 }
|
|
542
|
+
? { id, label: t(language, "providerLabelCustom", { display, status }), priority: 0, enabled }
|
|
534
543
|
: builtin
|
|
535
|
-
? { id, label: t(language, "providerLabelBuiltin", { display, status }), priority: 1 }
|
|
536
|
-
: { id, label: t(language, "providerLabelCustom", { display, status }), priority: 2 });
|
|
544
|
+
? { id, label: t(language, "providerLabelBuiltin", { display, status }), priority: 1, enabled }
|
|
545
|
+
: { id, label: t(language, "providerLabelCustom", { display, status }), priority: 2, enabled });
|
|
537
546
|
}
|
|
538
|
-
return choices.sort((a, b) => a.priority - b.priority || a.id.localeCompare(b.id));
|
|
547
|
+
return choices.sort((a, b) => Number(b.enabled) - Number(a.enabled) || a.priority - b.priority || a.id.localeCompare(b.id));
|
|
539
548
|
}
|
|
540
549
|
|
|
541
550
|
function buildProviderPanelItems(
|
|
@@ -589,7 +598,7 @@ function buildProviderPanelItems(
|
|
|
589
598
|
export function buildBuiltinProviderChoices(language: UiLanguage, builtinProviderIds: Iterable<string>): ProviderChoice[] {
|
|
590
599
|
return [...new Set(builtinProviderIds)]
|
|
591
600
|
.sort((a, b) => a.localeCompare(b))
|
|
592
|
-
.map((id) => ({ id, label: t(language, "providerLabelBuiltin", { display: id, status: t(language, "providerTagBuiltin") }), priority: 0 }));
|
|
601
|
+
.map((id) => ({ id, label: t(language, "providerLabelBuiltin", { display: id, status: t(language, "providerTagBuiltin") }), priority: 0, enabled: true }));
|
|
593
602
|
}
|
|
594
603
|
|
|
595
604
|
async function chooseBuiltinProvider(ctx: ExtensionCommandContext, language: UiLanguage): Promise<{ id: string; isNew: false } | undefined> {
|
|
@@ -655,6 +664,7 @@ function draftPanelItem(language: UiLanguage, draft: ProviderDraft, selected: Ma
|
|
|
655
664
|
],
|
|
656
665
|
modelDetails: [
|
|
657
666
|
t(language, "panelSummaryModels", { chat, classifier, image }),
|
|
667
|
+
...(draft.hiddenRemoteIds?.length ? [t(language, "panelHiddenRemoteCount", { count: draft.hiddenRemoteIds.length })] : []),
|
|
658
668
|
t(language, "detailFetchHint"),
|
|
659
669
|
...(adoptableCount ? [t(language, "panelTakeoverCount", { count: adoptableCount })] : []),
|
|
660
670
|
],
|
|
@@ -795,20 +805,50 @@ export function registerModelConfigCommand(pi: ExtensionAPI, startupIssues: Star
|
|
|
795
805
|
const draft = draftFrom(target.id, oldProvider, oldConfig, target.isNew);
|
|
796
806
|
const baselineDraft = structuredClone(draft);
|
|
797
807
|
const managedBefore = previousManaged(oldProvider, oldConfig);
|
|
798
|
-
const baselineSelected = structuredClone(managedBefore);
|
|
799
808
|
const persistedMetadata = new Map((oldConfig?.modelMetadata ?? []).map((entry) => [`${entry.type}\0${entry.id}`, entry]));
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
809
|
+
const parseConfig: ProviderConnectionConfig = oldConfig ?? {
|
|
810
|
+
authMode: draft.authMode === "temporary" ? "reuse-pi" : draft.authMode,
|
|
811
|
+
chatApi: draft.chatApi,
|
|
812
|
+
};
|
|
813
|
+
// models.json rows outside managedChatIds are directly editable and become
|
|
814
|
+
// managed on the next save; only runtime catalog rows stay read-only.
|
|
815
|
+
const managedChatIdSet = new Set(oldConfig?.managedChatIds ?? []);
|
|
816
|
+
const externalModels: ManagedModel[] = [];
|
|
817
|
+
const externalMetadata = new Map<string, ModelMetadataEntry>();
|
|
818
|
+
for (const raw of oldProvider?.models ?? []) {
|
|
819
|
+
if (typeof raw.id !== "string" || managedChatIdSet.has(raw.id)) continue;
|
|
820
|
+
const model = parseManagedChat(raw, oldProvider!, parseConfig, true);
|
|
821
|
+
if (!model) continue;
|
|
822
|
+
externalModels.push(model);
|
|
823
|
+
const explicitMap = typeof raw.thinkingLevelMap === "object" && raw.thinkingLevelMap !== null && !Array.isArray(raw.thinkingLevelMap);
|
|
824
|
+
externalMetadata.set(model.id, persistedMetadata.get(`${model.type}\0${model.id}`) ?? {
|
|
825
|
+
type: model.type,
|
|
826
|
+
id: model.id,
|
|
827
|
+
contextSource: model.type === "image" ? undefined : typeof raw.contextWindow === "number" ? "existing" : "compat",
|
|
828
|
+
maxTokensSource: model.type === "chat" ? typeof raw.maxTokens === "number" ? "existing" : "compat" : undefined,
|
|
829
|
+
thinkingSource: model.type === "chat" ? explicitMap ? "existing" as const : protocolThinkingMap(model.api) ? "protocol" as const : undefined : undefined,
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
const metadataEntryFor = (model: ManagedModel, raw: Record<string, unknown> | undefined, fallback: ModelMetadataEntry | undefined): ModelMetadataEntry => fallback ?? {
|
|
833
|
+
type: model.type,
|
|
834
|
+
id: model.id,
|
|
835
|
+
contextSource: model.type === "image" ? undefined : typeof raw?.contextWindow === "number" ? "existing" : "compat",
|
|
836
|
+
maxTokensSource: model.type === "chat" ? typeof raw?.maxTokens === "number" ? "existing" : "compat" : undefined,
|
|
837
|
+
thinkingSource: model.type === "chat" && typeof raw?.thinkingLevelMap === "object" && raw.thinkingLevelMap !== null && !Array.isArray(raw.thinkingLevelMap) ? "existing" as const : undefined,
|
|
838
|
+
};
|
|
839
|
+
let selectedMetadata: ModelMetadataEntry[] = [
|
|
840
|
+
...managedBefore.map((model) => {
|
|
841
|
+
const raw = model.type === "chat" ? oldProvider?.models?.find((entry) => entry.id === model.id) : oldConfig?.models?.find((entry) => entry.type === model.type && entry.id === model.id);
|
|
842
|
+
return structuredClone(metadataEntryFor(model, raw as Record<string, unknown> | undefined, persistedMetadata.get(`${model.type}\0${model.id}`)));
|
|
843
|
+
}),
|
|
844
|
+
...externalModels.map((model) => structuredClone(externalMetadata.get(model.id)!)),
|
|
845
|
+
];
|
|
846
|
+
const baselineSelected = structuredClone([...managedBefore, ...externalModels]);
|
|
807
847
|
const baselineMetadata = structuredClone(selectedMetadata);
|
|
808
848
|
|
|
809
|
-
let selectedDraft = structuredClone(
|
|
810
|
-
const adoptedChatIds = new Set<string>();
|
|
849
|
+
let selectedDraft = structuredClone(baselineSelected);
|
|
811
850
|
let cachedRemote: CandidateModel[] = [];
|
|
851
|
+
let fetchInFlight = false;
|
|
812
852
|
let page: "connection" | "model" | "preview" = "connection";
|
|
813
853
|
let previewOrigin: "connection" | "model" = "model";
|
|
814
854
|
const connectionState: MenuPanelState = { focus: "list", actionIndex: 0, detailScroll: 0 };
|
|
@@ -816,18 +856,14 @@ export function registerModelConfigCommand(pi: ExtensionAPI, startupIssues: Star
|
|
|
816
856
|
const previewState: MenuPanelState = { selectedId: "provider", focus: "list", actionIndex: 0, detailScroll: 0 };
|
|
817
857
|
activeSecrets.push(draft.apiKeyRaw);
|
|
818
858
|
|
|
819
|
-
const adoptableIds = () =>
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
.filter((model) => typeof model.id === "string" && !alreadyManaged.has(model.id) && !adoptedChatIds.has(model.id))
|
|
823
|
-
.map((model) => model.id as string);
|
|
824
|
-
};
|
|
859
|
+
const adoptableIds = () => externalModels
|
|
860
|
+
.filter((model) => !selectedDraft.some((selected) => selected.id === model.id && selected.type === model.type))
|
|
861
|
+
.map((model) => model.id);
|
|
825
862
|
const selectionFingerprint = (models: ManagedModel[]) => JSON.stringify([...models].sort((a, b) => identityOf(a, target.id).localeCompare(identityOf(b, target.id))));
|
|
826
863
|
const metadataFingerprint = (entries: ModelMetadataEntry[]) => JSON.stringify([...entries].sort((a, b) => `${a.type}\0${a.id}`.localeCompare(`${b.type}\0${b.id}`)));
|
|
827
864
|
const isDirty = () => JSON.stringify(draft) !== JSON.stringify(baselineDraft)
|
|
828
865
|
|| selectionFingerprint(selectedDraft) !== selectionFingerprint(baselineSelected)
|
|
829
|
-
|| metadataFingerprint(selectedMetadata) !== metadataFingerprint(baselineMetadata)
|
|
830
|
-
|| adoptedChatIds.size > 0;
|
|
866
|
+
|| metadataFingerprint(selectedMetadata) !== metadataFingerprint(baselineMetadata);
|
|
831
867
|
const preparePreview = async (): Promise<boolean> => {
|
|
832
868
|
if (draft.authMode === "temporary") {
|
|
833
869
|
ctx.ui.notify(t(language, "temporarySaveBlocked"), "warning");
|
|
@@ -835,7 +871,7 @@ export function registerModelConfigCommand(pi: ExtensionAPI, startupIssues: Star
|
|
|
835
871
|
}
|
|
836
872
|
try { validateSelection(draft, selectedDraft, installedModels(ctx, target.id), oldProvider); }
|
|
837
873
|
catch (error) { ctx.ui.notify(displayError(language, error, activeSecrets), "error"); return false; }
|
|
838
|
-
if (
|
|
874
|
+
if (baselineSelected.length > 0 && selectedDraft.length === 0) {
|
|
839
875
|
const empty = await showConfirmationPanel(ctx, {
|
|
840
876
|
title: t(language, "emptyTitle"), summary: [t(language, "emptyBody")], severity: "error",
|
|
841
877
|
defaultActionIndex: 0, cancelActionId: "cancel",
|
|
@@ -849,10 +885,11 @@ export function registerModelConfigCommand(pi: ExtensionAPI, startupIssues: Star
|
|
|
849
885
|
return true;
|
|
850
886
|
};
|
|
851
887
|
const adoptExisting = async (): Promise<boolean> => {
|
|
852
|
-
const
|
|
853
|
-
|
|
888
|
+
const selectedKeys = new Set(selectedDraft.map((model) => identityOf(model, target.id)));
|
|
889
|
+
const missing = externalModels.filter((model) => !selectedKeys.has(identityOf(model, target.id)));
|
|
890
|
+
if (!missing.length) { ctx.ui.notify(t(language, "takeoverModelsNone"), "info"); return false; }
|
|
854
891
|
const action = await showConfirmationPanel(ctx, {
|
|
855
|
-
title: t(language, "takeoverModelsTitle"), summary: [t(language, "takeoverModelsBody", { provider: target.id, count:
|
|
892
|
+
title: t(language, "takeoverModelsTitle"), summary: [t(language, "takeoverModelsBody", { provider: target.id, count: missing.length })],
|
|
856
893
|
details: [t(language, "diffSafety")], severity: "warning", defaultActionIndex: 0, cancelActionId: "cancel",
|
|
857
894
|
actions: [
|
|
858
895
|
{ id: "cancel", label: t(language, "confirmCancel") },
|
|
@@ -860,26 +897,25 @@ export function registerModelConfigCommand(pi: ExtensionAPI, startupIssues: Star
|
|
|
860
897
|
],
|
|
861
898
|
});
|
|
862
899
|
if (action !== "confirm") return false;
|
|
863
|
-
const
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
const selectedKeys = new Set(selectedDraft.map((model) => identityOf(model, target.id)));
|
|
868
|
-
for (const raw of oldProvider?.models ?? []) {
|
|
869
|
-
if (typeof raw.id !== "string" || !ids.includes(raw.id)) continue;
|
|
870
|
-
const model = parseManagedChat(raw, oldProvider!, parseConfig, true);
|
|
871
|
-
if (model && !selectedKeys.has(identityOf(model, target.id))) selectedDraft.push(model);
|
|
872
|
-
adoptedChatIds.add(raw.id);
|
|
900
|
+
for (const model of missing) {
|
|
901
|
+
selectedDraft.push(structuredClone(model));
|
|
902
|
+
const metadata = externalMetadata.get(model.id)!;
|
|
903
|
+
if (!selectedMetadata.some((entry) => entry.type === metadata.type && entry.id === metadata.id)) selectedMetadata.push(structuredClone(metadata));
|
|
873
904
|
}
|
|
874
905
|
return true;
|
|
875
906
|
};
|
|
876
907
|
|
|
877
908
|
providerSession: while (true) {
|
|
878
909
|
if (page === "connection") {
|
|
879
|
-
const connection = await editConnectionPanel(ctx, language, draft, connectionState, adoptableIds().length > 0);
|
|
910
|
+
const connection = await editConnectionPanel(ctx, language, draft, connectionState, adoptableIds().length > 0, draft.hiddenRemoteIds?.length ?? 0);
|
|
880
911
|
activeSecrets.push(draft.apiKeyRaw, draft.temporaryKey);
|
|
881
912
|
if (connection === "models") { page = "model"; continue; }
|
|
882
913
|
if (connection === "takeover") { if (await adoptExisting()) page = "model"; continue; }
|
|
914
|
+
if (connection === "restoreHidden") {
|
|
915
|
+
const restored = await showRestoreHiddenPanel(ctx, language, draft.hiddenRemoteIds ?? []);
|
|
916
|
+
if (restored.action === "apply") draft.hiddenRemoteIds = (draft.hiddenRemoteIds ?? []).filter((key) => !restored.restored.includes(key));
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
883
919
|
if (connection === "testModels") {
|
|
884
920
|
if (!draft.enabled) { ctx.ui.notify(t(language, "testProviderDisabled"), "warning"); continue; }
|
|
885
921
|
if (draft.authMode === "temporary" || isDirty()) { ctx.ui.notify(t(language, "testDraftDirty"), "warning"); continue; }
|
|
@@ -911,32 +947,15 @@ export function registerModelConfigCommand(pi: ExtensionAPI, startupIssues: Star
|
|
|
911
947
|
if (page === "model") {
|
|
912
948
|
const installedRaw = installedModels(ctx, target.id);
|
|
913
949
|
const installed = installedRaw.map(candidateFromInstalled);
|
|
914
|
-
const
|
|
915
|
-
const
|
|
916
|
-
|
|
917
|
-
chatApi: draft.chatApi,
|
|
918
|
-
};
|
|
919
|
-
const external = (oldProvider?.models ?? [])
|
|
920
|
-
.filter((model) => typeof model.id === "string" && !managedChatIds.has(model.id))
|
|
921
|
-
.map((model) => parseManagedChat(model, oldProvider!, parseConfig, adoptedChatIds.has(typeof model.id === "string" ? model.id : "")))
|
|
922
|
-
.filter((model): model is ManagedModel => model !== undefined)
|
|
923
|
-
.map((model) => {
|
|
924
|
-
const raw = oldProvider?.models?.find((entry) => entry.id === model.id);
|
|
925
|
-
const explicitMap = typeof raw?.thinkingLevelMap === "object" && raw.thinkingLevelMap !== null && !Array.isArray(raw.thinkingLevelMap);
|
|
926
|
-
const defaultThinkingSource = model.type === "chat"
|
|
927
|
-
? explicitMap ? "existing" as const : adoptedChatIds.has(model.id) && protocolThinkingMap(model.api) ? "protocol" as const : undefined
|
|
928
|
-
: undefined;
|
|
929
|
-
const metadata = persistedMetadata.get(`${model.type}\0${model.id}`) ?? { type: model.type, id: model.id, contextSource: model.type === "image" ? undefined : "compat", maxTokensSource: model.type === "chat" ? "compat" : undefined, thinkingSource: defaultThinkingSource };
|
|
930
|
-
return adoptedChatIds.has(model.id) ? candidateFromManaged(model, metadata) : candidateFromExternal(model, metadata);
|
|
931
|
-
});
|
|
932
|
-
const candidates = mergeCandidates(target.id, [...external, ...installed], managedBefore.map((model) => candidateFromManaged(model, persistedMetadata.get(`${model.type}\0${model.id}`))), cachedRemote);
|
|
950
|
+
const external = externalModels.map((model) => candidateFromExternal(model, externalMetadata.get(model.id)));
|
|
951
|
+
const visibleRemote = cachedRemote.filter((model) => !isHiddenRemote(model, draft.hiddenRemoteIds ?? []));
|
|
952
|
+
const candidates = mergeCandidates(target.id, [...external, ...installed], managedBefore.map((model) => candidateFromManaged(model, persistedMetadata.get(`${model.type}\0${model.id}`))), visibleRemote);
|
|
933
953
|
const selection = await selectModels(ctx, language, target.id, draft, candidates, selectedDraft, modelState, selectedMetadata);
|
|
934
954
|
selectedDraft = selection.selected;
|
|
935
955
|
selectedMetadata = selection.metadata.map((entry) => ({ ...(selectedMetadata.find((old) => old.type === entry.type && old.id === entry.id) ?? {}), ...entry }));
|
|
936
956
|
if (selection.action === "back") { page = "connection"; continue; }
|
|
937
957
|
if (selection.action === "edit") {
|
|
938
958
|
const editTarget = selection.target;
|
|
939
|
-
if (editTarget?.readOnlyReason === "external-config") { ctx.ui.notify(t(language, "editorExternalTakeover"), "warning"); continue; }
|
|
940
959
|
const managedIndex = editTarget ? selectedDraft.findIndex((model) => model.id === editTarget.id && model.type === editTarget.type) : -1;
|
|
941
960
|
if (managedIndex < 0) { ctx.ui.notify(t(language, "editorManagedOnly"), "warning"); continue; }
|
|
942
961
|
const model = selectedDraft[managedIndex]!;
|
|
@@ -983,6 +1002,9 @@ export function registerModelConfigCommand(pi: ExtensionAPI, startupIssues: Star
|
|
|
983
1002
|
continue;
|
|
984
1003
|
}
|
|
985
1004
|
if (selection.action === "fetch") {
|
|
1005
|
+
if (fetchInFlight) continue;
|
|
1006
|
+
fetchInFlight = true;
|
|
1007
|
+
try {
|
|
986
1008
|
const fallbackBases = [
|
|
987
1009
|
...(oldProvider?.models ?? []).map((model) => typeof model.baseUrl === "string" ? model.baseUrl : undefined),
|
|
988
1010
|
...installedRaw.map((model) => model.baseUrl),
|
|
@@ -1025,6 +1047,7 @@ export function registerModelConfigCommand(pi: ExtensionAPI, startupIssues: Star
|
|
|
1025
1047
|
draft.discoveryUrl = result.value!.url;
|
|
1026
1048
|
cachedRemote = [...result.value!.models];
|
|
1027
1049
|
continue;
|
|
1050
|
+
} finally { fetchInFlight = false; }
|
|
1028
1051
|
}
|
|
1029
1052
|
if (await preparePreview()) { previewOrigin = "model"; page = "preview"; }
|
|
1030
1053
|
continue;
|
|
@@ -1045,12 +1068,9 @@ export function registerModelConfigCommand(pi: ExtensionAPI, startupIssues: Star
|
|
|
1045
1068
|
if (discard === "discard") continue mainLoop;
|
|
1046
1069
|
continue;
|
|
1047
1070
|
}
|
|
1048
|
-
|
|
1049
|
-
const risk = compatItems.length ? [t(language, "compatSaveRisk", { items: compatItems.join(", ") })] : [];
|
|
1050
|
-
const confirmed = await confirmAction(ctx, language, t(language, "finalSaveTitle"), [t(language, "finalSaveBody"), ...risk], [...details.providerDetails, ...details.modelDetails], t(language, "confirmSave"));
|
|
1051
|
-
if (!confirmed) continue;
|
|
1071
|
+
// Preview is the sole save confirmation: Ctrl+S there commits the reviewed draft.
|
|
1052
1072
|
try {
|
|
1053
|
-
const next = applyChanges(files, target.id, draft, selectedDraft,
|
|
1073
|
+
const next = applyChanges(files, target.id, draft, selectedDraft, new Set(externalModels.map((model) => model.id)), selectedMetadata);
|
|
1054
1074
|
const transaction = writeConfigTransaction(files.modelsSnapshot, files.configSnapshot, next.models, next.config);
|
|
1055
1075
|
ctx.ui.notify(t(language, "saveSuccess", { count: transaction.backups.length }), "info");
|
|
1056
1076
|
continue mainLoop;
|
package/src/config.ts
CHANGED
|
@@ -72,6 +72,7 @@ export function parseModelConfig(source: ConfigSnapshot): ModelConfigFile {
|
|
|
72
72
|
if (provider.imageApi !== undefined && !IMAGE_API_NAMES.includes(String(provider.imageApi) as typeof IMAGE_API_NAMES[number])) {
|
|
73
73
|
throw mcError("MCFG_E_UNSUPPORTED_API", { identity: `${id}/image`, api: String(provider.imageApi) });
|
|
74
74
|
}
|
|
75
|
+
if (provider.hiddenRemoteIds !== undefined && (!Array.isArray(provider.hiddenRemoteIds) || provider.hiddenRemoteIds.some((modelId) => typeof modelId !== "string"))) throw mcError("MCFG_E_PROVIDER_SHAPE", { file: "model-config.json", provider: id });
|
|
75
76
|
if (provider.models !== undefined && !Array.isArray(provider.models)) throw mcError("MCFG_E_MODELS_ARRAY", { file: "model-config.json", provider: id });
|
|
76
77
|
if (Array.isArray(provider.models) && provider.models.some((model) => !isRecord(model))) throw mcError("MCFG_E_MODELS_ARRAY", { file: "model-config.json", provider: id });
|
|
77
78
|
for (const model of Array.isArray(provider.models) ? provider.models : []) {
|
package/src/i18n.ts
CHANGED
|
@@ -12,7 +12,7 @@ const ZH = {
|
|
|
12
12
|
languageEn: "English",
|
|
13
13
|
languageSaved: "界面语言已保存:{language}(立即生效)",
|
|
14
14
|
tuiRequired: "/model-config 需要交互式 TUI;RPC/print/json 模式不支持自定义多选界面",
|
|
15
|
-
startupIssues: "
|
|
15
|
+
startupIssues: "部分服务商/模型条目存在配置问题(无效条目已跳过,可继续编辑修复):{issues}{more}",
|
|
16
16
|
startupMore: ";另有 {count} 项",
|
|
17
17
|
newProviderTitle: "选择内置服务商(内置不代表已认证或可用)",
|
|
18
18
|
providerTagCustom: "自定义",
|
|
@@ -38,9 +38,16 @@ const ZH = {
|
|
|
38
38
|
detailConnectionTitle: "连接摘要",
|
|
39
39
|
detailModelsTitle: "模型概况",
|
|
40
40
|
detailFetchHint: "进入“配置模型”后按 Ctrl+R 获取远程模型列表",
|
|
41
|
+
detailRestoreHidden: "[Ctrl+H] 恢复隐藏模型({count})",
|
|
42
|
+
panelHiddenRemoteCount: "已隐藏远程模型:{count}",
|
|
43
|
+
restoreHiddenTitle: "恢复隐藏远程模型",
|
|
44
|
+
restoreHiddenItemDetail: "勾选后恢复到后续远程获取结果;保存前仅修改内存草稿。",
|
|
45
|
+
restoreHiddenAll: "[Ctrl+A] 全部恢复",
|
|
46
|
+
restoreHiddenApply: "[Ctrl+S] 应用恢复",
|
|
47
|
+
restoreHiddenBack: "[Esc] 返回",
|
|
41
48
|
detailEditConnection: "[Ctrl+E] 编辑连接",
|
|
42
49
|
detailConfigureModels: "[Ctrl+M] 配置模型",
|
|
43
|
-
detailTakeover: "[Ctrl+T]
|
|
50
|
+
detailTakeover: "[Ctrl+T] 全选并纳入管理",
|
|
44
51
|
detailDisable: "[Ctrl+X] 禁用服务商",
|
|
45
52
|
detailEnable: "[Ctrl+X] 启用服务商",
|
|
46
53
|
detailTestModels: "[Ctrl+P] 批量测试模型",
|
|
@@ -95,11 +102,11 @@ const ZH = {
|
|
|
95
102
|
modelDeselectFiltered: "[Ctrl+D] 取消勾选搜索结果",
|
|
96
103
|
modelCycleType: "[Ctrl+T] 切换模型类型",
|
|
97
104
|
modelEditParameters: "[Ctrl+E] 编辑模型参数",
|
|
98
|
-
|
|
99
|
-
editorManagedOnly: "仅当前草稿中已勾选、由本插件管理的模型可编辑参数",
|
|
105
|
+
editorManagedOnly: "仅当前草稿中已勾选的模型可编辑参数;请先勾选再编辑",
|
|
100
106
|
modelProbeContext: "[Ctrl+K] 探测上下文容量",
|
|
101
107
|
modelProbeThinking: "[Ctrl+L] 探测思考档位",
|
|
102
108
|
modelFetch: "[Ctrl+R] 获取远程模型列表",
|
|
109
|
+
modelHideRemote: "[Ctrl+Del] 隐藏远程模型",
|
|
103
110
|
modelPreview: "[Ctrl+S] 预览更改",
|
|
104
111
|
modelBack: "[Esc] 返回服务商",
|
|
105
112
|
addProviderTitle: "添加服务商",
|
|
@@ -112,7 +119,7 @@ const ZH = {
|
|
|
112
119
|
discoveryRequiredEdit: "进入编辑连接设置",
|
|
113
120
|
discoveryRequiredCancel: "取消获取",
|
|
114
121
|
dirtyLeaveTitle: "放弃尚未保存的更改?",
|
|
115
|
-
dirtyLeaveBody: "
|
|
122
|
+
dirtyLeaveBody: "返回总览会放弃当前服务商的连接草稿和模型选择;不会写入配置。",
|
|
116
123
|
confirmCancel: "[Esc] 取消/返回",
|
|
117
124
|
confirmSave: "[Ctrl+S] 确认保存",
|
|
118
125
|
confirmEmpty: "[Ctrl+S] 确认清空",
|
|
@@ -178,7 +185,7 @@ const ZH = {
|
|
|
178
185
|
panelSummaryAuth: "认证方式:{value}",
|
|
179
186
|
panelSummaryProxy: "专属代理:{value}",
|
|
180
187
|
panelSummaryModels: "模型:聊天 {chat} · 判定 {classifier} · 图像生成 {image}",
|
|
181
|
-
panelTakeoverCount: "
|
|
188
|
+
panelTakeoverCount: "尚未纳入管理的 models.json chat:{count}",
|
|
182
189
|
panelDescName: "设置界面显示名称;不会改变服务商 ID。",
|
|
183
190
|
panelDescId: "配置标识,只读。provider/model identity 使用此 ID。",
|
|
184
191
|
panelDescOwnership: "决定 classifier/image 是否由本插件注册;不要接管 Pi 内置服务商。",
|
|
@@ -222,11 +229,10 @@ const ZH = {
|
|
|
222
229
|
probeMaxConflict: "探测下限小于当前maxTokens;不会自动调整maxTokens,请等待逐模型参数编辑功能",
|
|
223
230
|
panelModelBaseUrl: "Base URL:{value}",
|
|
224
231
|
panelModelReadonly: "此模型只读,不能在这里修改或移除。",
|
|
225
|
-
panelModelReadonlyExternal: "此模型来自 models.json,但尚未由本插件管理;可先执行“接管已有模型”。",
|
|
226
232
|
panelModelReadonlyRuntime: "此模型来自 Pi 内置目录或其他运行时扩展;本插件不能把取消勾选伪装成删除。",
|
|
227
|
-
takeoverModelsTitle: "
|
|
228
|
-
takeoverModelsBody: "
|
|
229
|
-
takeoverModelsNone: "
|
|
233
|
+
takeoverModelsTitle: "全选 models.json 模型?",
|
|
234
|
+
takeoverModelsBody: "将勾选服务商 {provider} 在 models.json 中尚未勾选的 {count} 个模型,并在最终保存后纳入 managedChatIds。此操作只修改内存草稿;取消、返回或放弃不会写盘。Pi 内置目录和其他运行时扩展模型不在范围。",
|
|
235
|
+
takeoverModelsNone: "当前服务商没有未勾选的 models.json 模型",
|
|
230
236
|
previewConfirmSave: "[Ctrl+S] 确认保存",
|
|
231
237
|
previewBackEdit: "[Esc] 返回继续编辑",
|
|
232
238
|
previewDiscard: "[Ctrl+D] 放弃全部更改",
|
|
@@ -311,10 +317,10 @@ const ZH = {
|
|
|
311
317
|
selectorNarrow: "终端过窄;请扩宽后重试,Esc 返回",
|
|
312
318
|
selectorNoMatches: " 没有匹配的模型",
|
|
313
319
|
selectorReadonlyStatus: "此模型不由本插件管理,当前不能在这里修改或移除",
|
|
314
|
-
selectorReadonlyExternal: "此模型来自 models.json,需先使用“接管已有模型”后才能修改或移除",
|
|
315
320
|
selectorReadonlyRuntime: "此模型来自 Pi 内置目录或其他运行时扩展,不能在这里移除",
|
|
316
321
|
selectorTypeConflict: "不能切换为{type}:{identity} 已存在",
|
|
317
322
|
selectorReadonlyTag: "只读",
|
|
323
|
+
selectorRemoteHidden: "已隐藏远程模型 {id};保存后后续获取也会隐藏",
|
|
318
324
|
selectorTypeChat: "聊天",
|
|
319
325
|
selectorTypeClassifier: "判定",
|
|
320
326
|
selectorTypeImage: "图像生成",
|
|
@@ -349,7 +355,7 @@ const EN = {
|
|
|
349
355
|
languageEn: "English",
|
|
350
356
|
languageSaved: "UI language saved: {language} (effective immediately)",
|
|
351
357
|
tuiRequired: "/model-config requires the interactive TUI; custom multi-select is unavailable in RPC/print/JSON modes",
|
|
352
|
-
startupIssues: "Some providers
|
|
358
|
+
startupIssues: "Some providers/model entries have configuration problems (invalid entries were skipped; keep editing to fix): {issues}{more}",
|
|
353
359
|
startupMore: "; {count} more",
|
|
354
360
|
newProviderTitle: "Choose a Built-in Provider (built-in does not mean authenticated or available)",
|
|
355
361
|
providerTagCustom: "Custom",
|
|
@@ -375,9 +381,16 @@ const EN = {
|
|
|
375
381
|
detailConnectionTitle: "Connection Summary",
|
|
376
382
|
detailModelsTitle: "Model Overview",
|
|
377
383
|
detailFetchHint: "Open Configure models, then press Ctrl+R to fetch the remote model list",
|
|
384
|
+
detailRestoreHidden: "[Ctrl+H] Restore hidden models ({count})",
|
|
385
|
+
panelHiddenRemoteCount: "Hidden remote models: {count}",
|
|
386
|
+
restoreHiddenTitle: "Restore hidden remote models",
|
|
387
|
+
restoreHiddenItemDetail: "Checked entries return to future remote fetch results; this only changes the in-memory draft until save.",
|
|
388
|
+
restoreHiddenAll: "[Ctrl+A] Restore all",
|
|
389
|
+
restoreHiddenApply: "[Ctrl+S] Apply restore",
|
|
390
|
+
restoreHiddenBack: "[Esc] Back",
|
|
378
391
|
detailEditConnection: "[Ctrl+E] Edit connection",
|
|
379
392
|
detailConfigureModels: "[Ctrl+M] Configure models",
|
|
380
|
-
detailTakeover: "[Ctrl+T]
|
|
393
|
+
detailTakeover: "[Ctrl+T] Select all and manage",
|
|
381
394
|
detailDisable: "[Ctrl+X] Disable provider",
|
|
382
395
|
detailEnable: "[Ctrl+X] Enable provider",
|
|
383
396
|
detailTestModels: "[Ctrl+P] Batch test models",
|
|
@@ -432,11 +445,11 @@ const EN = {
|
|
|
432
445
|
modelDeselectFiltered: "[Ctrl+D] Uncheck search results",
|
|
433
446
|
modelCycleType: "[Ctrl+T] Change model type",
|
|
434
447
|
modelEditParameters: "[Ctrl+E] Edit model parameters",
|
|
435
|
-
|
|
436
|
-
editorManagedOnly: "Only checked models managed by this plugin in the current draft can be edited",
|
|
448
|
+
editorManagedOnly: "Only checked models in the current draft can be edited; check the row first",
|
|
437
449
|
modelProbeContext: "[Ctrl+K] Probe context capacity",
|
|
438
450
|
modelProbeThinking: "[Ctrl+L] Probe thinking levels",
|
|
439
451
|
modelFetch: "[Ctrl+R] Fetch remote model list",
|
|
452
|
+
modelHideRemote: "[Ctrl+Del] Hide remote model",
|
|
440
453
|
modelPreview: "[Ctrl+S] Preview changes",
|
|
441
454
|
modelBack: "[Esc] Back to provider",
|
|
442
455
|
addProviderTitle: "Add Provider",
|
|
@@ -449,7 +462,7 @@ const EN = {
|
|
|
449
462
|
discoveryRequiredEdit: "Open connection settings",
|
|
450
463
|
discoveryRequiredCancel: "Cancel fetch",
|
|
451
464
|
dirtyLeaveTitle: "Discard unsaved changes?",
|
|
452
|
-
dirtyLeaveBody: "Returning to the overview discards this provider's connection draft
|
|
465
|
+
dirtyLeaveBody: "Returning to the overview discards this provider's connection draft and model selection. Nothing will be written.",
|
|
453
466
|
confirmCancel: "[Esc] Cancel / Back",
|
|
454
467
|
confirmSave: "[Ctrl+S] Confirm Save",
|
|
455
468
|
confirmEmpty: "[Ctrl+S] Confirm Empty",
|
|
@@ -515,7 +528,7 @@ const EN = {
|
|
|
515
528
|
panelSummaryAuth: "Authentication: {value}",
|
|
516
529
|
panelSummaryProxy: "Provider-only proxy: {value}",
|
|
517
530
|
panelSummaryModels: "Models: Chat {chat} · Classification {classifier} · Image generation {image}",
|
|
518
|
-
panelTakeoverCount: "
|
|
531
|
+
panelTakeoverCount: "models.json chat models not yet managed: {count}",
|
|
519
532
|
panelDescName: "Set the display name without changing the provider ID.",
|
|
520
533
|
panelDescId: "Read-only configuration identity used by provider/model identities.",
|
|
521
534
|
panelDescOwnership: "Controls classifier/image registration. Never take ownership of a Pi built-in provider.",
|
|
@@ -559,11 +572,10 @@ const EN = {
|
|
|
559
572
|
probeMaxConflict: "The probe lower bound is below current maxTokens. maxTokens will not be changed automatically; wait for per-model parameter editing.",
|
|
560
573
|
panelModelBaseUrl: "Base URL: {value}",
|
|
561
574
|
panelModelReadonly: "This model is read-only and cannot be changed or removed here.",
|
|
562
|
-
panelModelReadonlyExternal: "This model is in models.json but is not managed by this plugin yet; use Manage existing models first.",
|
|
563
575
|
panelModelReadonlyRuntime: "This model comes from Pi's built-in catalog or another runtime extension; unchecking it cannot be presented as deletion.",
|
|
564
|
-
takeoverModelsTitle: "
|
|
565
|
-
takeoverModelsBody: "This
|
|
566
|
-
takeoverModelsNone: "This provider has no models.json
|
|
576
|
+
takeoverModelsTitle: "Select all models.json models?",
|
|
577
|
+
takeoverModelsBody: "This checks the {count} unchecked model(s) of provider {provider} in models.json and includes them in managedChatIds after the final save. It changes the in-memory draft only; cancel, back, or discard writes nothing. Pi built-in catalogs and other runtime-extension models are outside this scope.",
|
|
578
|
+
takeoverModelsNone: "This provider has no unchecked models.json models",
|
|
567
579
|
previewConfirmSave: "[Ctrl+S] Confirm Save",
|
|
568
580
|
previewBackEdit: "[Esc] Back to Editing",
|
|
569
581
|
previewDiscard: "[Ctrl+D] Discard All Changes",
|
|
@@ -648,10 +660,10 @@ const EN = {
|
|
|
648
660
|
selectorNarrow: "Terminal too narrow; resize and retry, or press Esc to go back",
|
|
649
661
|
selectorNoMatches: " No matching models",
|
|
650
662
|
selectorReadonlyStatus: "This model is not managed by this plugin and cannot be changed or removed here",
|
|
651
|
-
selectorReadonlyExternal: "This model comes from models.json; use Manage existing models before changing or removing it",
|
|
652
663
|
selectorReadonlyRuntime: "This model comes from Pi's built-in catalog or another runtime extension and cannot be removed here",
|
|
653
664
|
selectorTypeConflict: "Cannot change to {type}: {identity} already exists",
|
|
654
665
|
selectorReadonlyTag: "read-only",
|
|
666
|
+
selectorRemoteHidden: "Remote model {id} is hidden and will remain hidden after saving and future fetches",
|
|
655
667
|
selectorTypeChat: "Chat",
|
|
656
668
|
selectorTypeClassifier: "Classification",
|
|
657
669
|
selectorTypeImage: "Image generation",
|
package/src/image-apis/common.ts
CHANGED
|
@@ -130,13 +130,13 @@ function privateHostname(hostname: string): boolean {
|
|
|
130
130
|
const low = Number.parseInt(mappedHex[2]!, 16);
|
|
131
131
|
normalized = `${high >>> 8}.${high & 0xff}.${low >>> 8}.${low & 0xff}`;
|
|
132
132
|
}
|
|
133
|
-
if (normalized === "localhost" || normalized.endsWith(".localhost") || normalized === "::1" || normalized === "0:0:0:0:0:0:0:1") return true;
|
|
133
|
+
if (normalized === "localhost" || normalized.endsWith(".localhost") || normalized === "::" || normalized === "0:0:0:0:0:0:0:0" || normalized === "::1" || normalized === "0:0:0:0:0:0:0:1") return true;
|
|
134
134
|
const ipv4 = normalized.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/u)?.slice(1).map(Number);
|
|
135
135
|
if (ipv4 && ipv4.every((part) => part >= 0 && part <= 255)) {
|
|
136
136
|
const [a, b] = ipv4;
|
|
137
137
|
return a === 0 || a === 10 || a === 127 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168;
|
|
138
138
|
}
|
|
139
|
-
return normalized.startsWith("fc") || normalized.startsWith("fd") ||
|
|
139
|
+
return normalized.startsWith("fc") || normalized.startsWith("fd") || /^(?:fe8|fe9|fea|feb)[0-9a-f]:/u.test(normalized);
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
export function validatePublicImageUrl(value: string): URL {
|
package/src/panel.ts
CHANGED
|
@@ -43,7 +43,7 @@ export type ProviderPanelResult =
|
|
|
43
43
|
|
|
44
44
|
export type ConnectionPanelResult =
|
|
45
45
|
| { action: "field"; fieldId: string }
|
|
46
|
-
| { action: "models" | "takeover" | "testModels" | "preview" | "back" };
|
|
46
|
+
| { action: "models" | "takeover" | "restoreHidden" | "testModels" | "preview" | "back" };
|
|
47
47
|
|
|
48
48
|
export type PreviewPanelResult = { action: "save" | "back" | "discard" };
|
|
49
49
|
|
|
@@ -434,11 +434,13 @@ export async function showConnectionPanel(
|
|
|
434
434
|
fields: PanelItem[],
|
|
435
435
|
state: MenuPanelState = { focus: "list", actionIndex: 0, detailScroll: 0 },
|
|
436
436
|
canTakeover = false,
|
|
437
|
+
hiddenRemoteCount = 0,
|
|
437
438
|
): Promise<ConnectionPanelResult> {
|
|
438
439
|
const actions: FooterAction[] = [
|
|
439
440
|
{ id: "edit", label: t(language, "connectionModify"), key: Key.enter },
|
|
440
441
|
{ id: "models", label: t(language, "detailConfigureModels"), key: Key.ctrl("m") },
|
|
441
442
|
...(canTakeover ? [{ id: "takeover", label: t(language, "detailTakeover"), key: Key.ctrl("t") }] : []),
|
|
443
|
+
...(hiddenRemoteCount > 0 ? [{ id: "restoreHidden", label: t(language, "detailRestoreHidden", { count: hiddenRemoteCount }), key: Key.ctrl("h") }] : []),
|
|
442
444
|
{ id: "testModels", label: t(language, "detailTestModels"), key: Key.ctrl("p") },
|
|
443
445
|
{ id: "preview", label: t(language, "connectionPreview"), key: Key.ctrl("s") },
|
|
444
446
|
{ id: "back", label: t(language, "connectionBack"), key: Key.escape },
|
|
@@ -448,10 +450,33 @@ export async function showConnectionPanel(
|
|
|
448
450
|
if (result.kind === "back" || result.id === "back") return { action: "back" };
|
|
449
451
|
if (result.kind === "item" && result.id) return { action: "field", fieldId: result.id };
|
|
450
452
|
if (result.id === "edit" && state.selectedId) return { action: "field", fieldId: state.selectedId };
|
|
451
|
-
if (result.id === "models" || result.id === "takeover" || result.id === "testModels" || result.id === "preview") return { action: result.id };
|
|
453
|
+
if (result.id === "models" || result.id === "takeover" || result.id === "restoreHidden" || result.id === "testModels" || result.id === "preview") return { action: result.id };
|
|
452
454
|
return { action: "back" };
|
|
453
455
|
}
|
|
454
456
|
|
|
457
|
+
export async function showRestoreHiddenPanel(
|
|
458
|
+
ctx: ExtensionCommandContext,
|
|
459
|
+
language: UiLanguage,
|
|
460
|
+
hiddenKeys: string[],
|
|
461
|
+
state: MenuPanelState = { focus: "list", actionIndex: 0, detailScroll: 0 },
|
|
462
|
+
): Promise<{ action: "apply" | "back"; restored: string[] }> {
|
|
463
|
+
const selected = new Set<string>();
|
|
464
|
+
const actions: FooterAction[] = [
|
|
465
|
+
{ id: "all", label: t(language, "restoreHiddenAll"), key: Key.ctrl("a") },
|
|
466
|
+
{ id: "apply", label: t(language, "restoreHiddenApply"), key: Key.ctrl("s") },
|
|
467
|
+
{ id: "back", label: t(language, "restoreHiddenBack"), key: Key.escape },
|
|
468
|
+
];
|
|
469
|
+
while (true) {
|
|
470
|
+
const items = hiddenKeys.map((key) => ({ id: key, label: `${selected.has(key) ? "[✓]" : "[ ]"} ${key.replace("\u0000", "/")}`, details: [t(language, "restoreHiddenItemDetail")] }));
|
|
471
|
+
const result = await ctx.ui.custom<{ kind: "item" | "action" | "language" | "back"; id?: string }>((tui, theme, _keys, done) =>
|
|
472
|
+
new MenuDetailPanel(tui, theme, language, t(language, "restoreHiddenTitle"), items, actions, state, true, done), MODEL_CONFIG_OVERLAY_OPTIONS);
|
|
473
|
+
if (result.kind === "back" || result.id === "back") return { action: "back", restored: [] };
|
|
474
|
+
if (result.kind === "item" && result.id) { if (selected.has(result.id)) selected.delete(result.id); else selected.add(result.id); continue; }
|
|
475
|
+
if (result.id === "all") { for (const hiddenKey of hiddenKeys) selected.add(hiddenKey); continue; }
|
|
476
|
+
if (result.id === "apply") return { action: "apply", restored: [...selected] };
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
455
480
|
export async function showPreviewPanel(
|
|
456
481
|
ctx: ExtensionCommandContext,
|
|
457
482
|
language: UiLanguage,
|
package/src/runtime.ts
CHANGED
|
@@ -159,34 +159,61 @@ function normalizeChatModels(providerId: string, provider: ModelsJsonProvider, c
|
|
|
159
159
|
}));
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
function validateRuntimeConfig(providerId: string, config: ProviderConnectionConfig, modelsProvider: ModelsJsonProvider | undefined): void {
|
|
162
|
+
function validateRuntimeConfig(providerId: string, config: ProviderConnectionConfig, modelsProvider: ModelsJsonProvider | undefined, issues: ProviderRegistrationIssue[] = []): void {
|
|
163
163
|
if (config.discoveryUrl) validateHttpUrl(config.discoveryUrl, `${providerId} Discovery URL`);
|
|
164
164
|
if (config.proxyUrl) validateHttpUrl(config.proxyUrl, `${providerId}.proxyUrl`);
|
|
165
165
|
const chatApis = new Set<string>();
|
|
166
166
|
for (const model of modelsProvider?.models ?? []) {
|
|
167
|
-
if (typeof model.id !== "string" || !model.id)
|
|
167
|
+
if (typeof model.id !== "string" || !model.id) {
|
|
168
|
+
issues.push({ providerId, error: mcError("MCFG_E_MODEL_SHAPE", { file: "models.json", provider: providerId }) });
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
168
171
|
if (model.type === "classifier" || model.type === "image") {
|
|
169
|
-
|
|
172
|
+
issues.push({ providerId, error: mcError("MCFG_E_TYPE_API", { identity: `${providerId}/chat/${model.id}`, api: String(model.api ?? model.type) }) });
|
|
173
|
+
continue;
|
|
170
174
|
}
|
|
171
175
|
const api = typeof model.api === "string" ? model.api : (modelsProvider?.api ?? config.chatApi ?? "openai-completions");
|
|
172
|
-
if (!CHAT_APIS.has(api))
|
|
176
|
+
if (!CHAT_APIS.has(api)) {
|
|
177
|
+
issues.push({ providerId, error: mcError("MCFG_E_UNSUPPORTED_API", { identity: `${providerId}/chat/${model.id}`, api }) });
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
173
180
|
chatApis.add(api);
|
|
174
|
-
|
|
175
|
-
|
|
181
|
+
// owned chat 缺失 context/max 由 normalizeChatModels 补 Pi 兼容默认(128000/16384),
|
|
182
|
+
// 不再作为错误阻断注册;来源标记在保存时记录为 compat。
|
|
183
|
+
if (config.owned) {
|
|
184
|
+
try { validateHttpUrl(String(model.baseUrl ?? modelsProvider?.baseUrl ?? ""), `${providerId}/${model.id} Base URL`); }
|
|
185
|
+
catch (error) { issues.push({ providerId, error }); continue; }
|
|
186
|
+
}
|
|
176
187
|
}
|
|
177
188
|
for (const model of config.models ?? []) {
|
|
178
189
|
const supported = model.type === "classifier" ? CLASSIFIER_APIS : model.type === "image" ? IMAGE_APIS : undefined;
|
|
179
|
-
if (!supported)
|
|
180
|
-
|
|
190
|
+
if (!supported) {
|
|
191
|
+
issues.push({ providerId, error: mcError("MCFG_E_MODEL_SHAPE", { file: "model-config.json", provider: providerId }) });
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (!supported.has(model.api)) {
|
|
195
|
+
issues.push({ providerId, error: mcError("MCFG_E_TYPE_API", { identity: `${providerId}/${model.type}/${model.id}`, api: model.api }) });
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (config.authMode === "anonymous" && model.type === "image" && SELF_IMAGE_APIS.has(model.api)) {
|
|
199
|
+
issues.push({ providerId, error: mcError("MCFG_E_ANONYMOUS_UNSUPPORTED", { identity: `${providerId}/image/${model.id}` }) });
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (config.owned && model.type === "classifier" && typeof model.contextWindow !== "number") {
|
|
203
|
+
issues.push({ providerId, error: mcError("MCFG_E_MODEL_SHAPE", { file: "model-config.json", provider: providerId }) });
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
181
206
|
const effectiveBaseUrl = model.type === "image"
|
|
182
207
|
? resolveImageBaseUrl(model.api, model.baseUrl, modelsProvider?.baseUrl)
|
|
183
208
|
: model.baseUrl ?? modelsProvider?.baseUrl;
|
|
184
|
-
if (config.owned && !effectiveBaseUrl)
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
209
|
+
if (config.owned && !effectiveBaseUrl) {
|
|
210
|
+
issues.push({ providerId, error: mcError("MCFG_E_BASE_URL_REQUIRED") });
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (config.owned && effectiveBaseUrl) {
|
|
214
|
+
try { validateHttpUrl(effectiveBaseUrl, `${providerId}/${model.id} Base URL`); }
|
|
215
|
+
catch (error) { issues.push({ providerId, error }); continue; }
|
|
188
216
|
}
|
|
189
|
-
if (config.owned && model.type === "classifier" && typeof model.contextWindow !== "number") throw mcError("MCFG_E_MODEL_SHAPE", { file: "model-config.json", provider: providerId });
|
|
190
217
|
}
|
|
191
218
|
if (!config.owned && (config.models?.length ?? 0) > 0) {
|
|
192
219
|
throw mcError("MCFG_E_NOT_OWNED", { provider: providerId });
|
|
@@ -202,14 +229,17 @@ function validateRuntimeConfig(providerId: string, config: ProviderConnectionCon
|
|
|
202
229
|
}
|
|
203
230
|
|
|
204
231
|
function registerOwnedProvider(pi: ExtensionAPI, providerId: string, config: ProviderConnectionConfig, modelsProvider: ModelsJsonProvider, language: UiLanguage): void {
|
|
205
|
-
const chatModels = normalizeChatModels(providerId, modelsProvider, config)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
: model.
|
|
212
|
-
|
|
232
|
+
const chatModels = normalizeChatModels(providerId, modelsProvider, config)
|
|
233
|
+
.filter((model) => typeof model.id === "string" && model.id && CHAT_APIS.has(model.api) && typeof model.contextWindow === "number" && typeof model.maxTokens === "number");
|
|
234
|
+
const extraModels = (config.models ?? [])
|
|
235
|
+
.map((model) => ({
|
|
236
|
+
...model,
|
|
237
|
+
provider: providerId,
|
|
238
|
+
baseUrl: model.type === "image"
|
|
239
|
+
? resolveImageBaseUrl(model.api, model.baseUrl, modelsProvider.baseUrl) ?? ""
|
|
240
|
+
: model.baseUrl ?? modelsProvider.baseUrl ?? "",
|
|
241
|
+
}))
|
|
242
|
+
.filter((model) => typeof model.id === "string" && model.id && (model.type !== "classifier" || typeof model.contextWindow === "number"));
|
|
213
243
|
const allModels = [...chatModels, ...extraModels];
|
|
214
244
|
if (allModels.length === 0) return;
|
|
215
245
|
const chatApis = new Set(chatModels.map((model) => model.api));
|
|
@@ -297,7 +327,7 @@ export function registerConfiguredProviders(
|
|
|
297
327
|
registerDisabledProvider(pi, providerId, config, modelsProvider);
|
|
298
328
|
continue;
|
|
299
329
|
}
|
|
300
|
-
validateRuntimeConfig(providerId, config, modelsProvider);
|
|
330
|
+
validateRuntimeConfig(providerId, config, modelsProvider, issues);
|
|
301
331
|
if (config.owned) {
|
|
302
332
|
if (!modelsProvider) throw mcError("MCFG_E_OWNED_PROVIDER_MISSING", { provider: providerId });
|
|
303
333
|
registerOwnedProvider(pi, providerId, config, modelsProvider, language);
|
package/src/selector.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { t, type UiLanguage } from "./i18n.ts";
|
|
|
15
15
|
import { COMPAT_CONTEXT_WINDOW, COMPAT_MAX_TOKENS } from "./discovery.ts";
|
|
16
16
|
import { MODEL_CONFIG_OVERLAY_OPTIONS, overlayViewportRows, renderActionFooter, renderPanelBox, type FooterAction } from "./panel.ts";
|
|
17
17
|
import type { CandidateModel, ManagedModel, ManagedModelType, ModelMetadataEntry, ProviderDraft, SelectionResult, ThinkingLevelMap, ThinkingMetadataSource } from "./types.ts";
|
|
18
|
-
import { identityOf } from "./types.ts";
|
|
18
|
+
import { hiddenRemoteKey, identityOf } from "./types.ts";
|
|
19
19
|
import { protocolThinkingMap } from "./thinking-defaults.ts";
|
|
20
20
|
import { describeThinking } from "./model-editor.ts";
|
|
21
21
|
|
|
@@ -76,8 +76,10 @@ export function candidateFromManaged(model: ManagedModel, metadata?: ModelMetada
|
|
|
76
76
|
return { ...cloneManaged(model), source: "managed", mutableType: false, contextSource: metadata?.contextSource ?? (model.type === "image" ? undefined : "existing"), maxTokensSource: metadata?.maxTokensSource ?? (model.type === "chat" ? "existing" : undefined), thinkingSource: metadata?.thinkingSource ?? (model.type === "chat" && model.thinkingLevelMap ? "existing" : undefined) } as CandidateModel;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/** A models.json row belongs to this provider configuration and is immediately editable.
|
|
80
|
+
* It becomes managed on the next save; only runtime catalog rows remain read-only. */
|
|
79
81
|
export function candidateFromExternal(model: ManagedModel, metadata?: ModelMetadataEntry): CandidateModel {
|
|
80
|
-
return { ...cloneManaged(model), source: "external", mutableType: false, contextSource: metadata?.contextSource ?? (model.type === "image" ? undefined : "existing"), maxTokensSource: metadata?.maxTokensSource ?? (model.type === "chat" ? "existing" : undefined), thinkingSource: metadata?.thinkingSource ?? (model.type === "chat" && model.thinkingLevelMap ? "existing" : undefined)
|
|
82
|
+
return { ...cloneManaged(model), source: "external", mutableType: false, contextSource: metadata?.contextSource ?? (model.type === "image" ? undefined : "existing"), maxTokensSource: metadata?.maxTokensSource ?? (model.type === "chat" ? "existing" : undefined), thinkingSource: metadata?.thinkingSource ?? (model.type === "chat" && model.thinkingLevelMap ? "existing" : undefined) } as CandidateModel;
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
export function mergeCandidates(provider: string, installed: CandidateModel[], managed: CandidateModel[], remote: CandidateModel[]): CandidateModel[] {
|
|
@@ -206,7 +208,7 @@ class ModelSelector implements Component, Focusable {
|
|
|
206
208
|
const row = this.filtered[this.index];
|
|
207
209
|
if (!row) return;
|
|
208
210
|
if (row.readOnly) {
|
|
209
|
-
this.status = t(this.language, row.readOnlyReason === "
|
|
211
|
+
this.status = t(this.language, row.readOnlyReason === "runtime-catalog" ? "selectorReadonlyRuntime" : "selectorReadonlyStatus");
|
|
210
212
|
this.tui.requestRender();
|
|
211
213
|
return;
|
|
212
214
|
}
|
|
@@ -216,6 +218,19 @@ class ModelSelector implements Component, Focusable {
|
|
|
216
218
|
this.tui.requestRender();
|
|
217
219
|
}
|
|
218
220
|
|
|
221
|
+
private hideCurrentRemote(): void {
|
|
222
|
+
const row = this.current();
|
|
223
|
+
if (!row || row.source !== "remote") return;
|
|
224
|
+
const hidden = new Set(this.draft.hiddenRemoteIds ?? []);
|
|
225
|
+
hidden.add(hiddenRemoteKey(row));
|
|
226
|
+
this.draft.hiddenRemoteIds = [...hidden].sort((a, b) => a.localeCompare(b));
|
|
227
|
+
this.selected.delete(this.key(row));
|
|
228
|
+
const index = this.rows.indexOf(row);
|
|
229
|
+
if (index >= 0) this.rows.splice(index, 1);
|
|
230
|
+
this.status = t(this.language, "selectorRemoteHidden", { id: row.id });
|
|
231
|
+
this.refresh();
|
|
232
|
+
}
|
|
233
|
+
|
|
219
234
|
private cycleType(): void {
|
|
220
235
|
const row = this.filtered[this.index];
|
|
221
236
|
if (!row?.mutableType) return;
|
|
@@ -275,6 +290,7 @@ class ModelSelector implements Component, Focusable {
|
|
|
275
290
|
{ id: "probe", label: t(this.language, "modelProbeContext"), key: Key.ctrl("k") },
|
|
276
291
|
{ id: "probeThinking", label: t(this.language, "modelProbeThinking"), key: Key.ctrl("l") },
|
|
277
292
|
{ id: "fetch", label: t(this.language, "modelFetch"), key: Key.ctrl("r") },
|
|
293
|
+
{ id: "hideRemote", label: t(this.language, "modelHideRemote"), key: Key.ctrl("delete"), enabled: this.current()?.source === "remote" },
|
|
278
294
|
{ id: "preview", label: t(this.language, "modelPreview"), key: Key.ctrl("s") },
|
|
279
295
|
{ id: "back", label: t(this.language, "modelBack"), key: Key.escape },
|
|
280
296
|
];
|
|
@@ -299,12 +315,14 @@ class ModelSelector implements Component, Focusable {
|
|
|
299
315
|
if (action.id === "selectFiltered") { for (const row of this.filtered) if (!row.readOnly) this.selected.add(this.key(row)); this.request(); return; }
|
|
300
316
|
if (action.id === "deselectFiltered") { for (const row of this.filtered) if (!row.readOnly) this.selected.delete(this.key(row)); this.request(); return; }
|
|
301
317
|
if (action.id === "cycleType") { this.cycleType(); return; }
|
|
318
|
+
if (action.id === "hideRemote") { this.hideCurrentRemote(); return; }
|
|
302
319
|
this.finish(action.id as SelectionResult["action"]);
|
|
303
320
|
}
|
|
304
321
|
|
|
305
322
|
handleInput(data: string): void {
|
|
306
323
|
if (data === "/") { this.state.focus = "search"; this.request(); return; }
|
|
307
324
|
if (matchesKey(data, Key.ctrl("r"))) { this.finish("fetch"); return; }
|
|
325
|
+
if (matchesKey(data, Key.ctrl("delete"))) { this.hideCurrentRemote(); return; }
|
|
308
326
|
if (matchesKey(data, Key.ctrl("e"))) { this.finish("edit"); return; }
|
|
309
327
|
if (matchesKey(data, Key.ctrl("k"))) { this.finish("probe"); return; }
|
|
310
328
|
if (matchesKey(data, Key.ctrl("l"))) { this.finish("probeThinking"); return; }
|
|
@@ -405,7 +423,7 @@ class ModelSelector implements Component, Focusable {
|
|
|
405
423
|
...(current.type === "image" ? [t(this.language, "panelModelOutput", { value: current.output.join(", ") || t(this.language, "valueUnset") })] : []),
|
|
406
424
|
t(this.language, "panelModelCost", { input: current.cost.input, output: current.cost.output, cacheRead: current.cost.cacheRead, cacheWrite: current.cost.cacheWrite }),
|
|
407
425
|
t(this.language, "panelModelBaseUrl", { value: current.baseUrl ?? t(this.language, "valueDefault") }),
|
|
408
|
-
...(current.readOnly ? [t(this.language, current.readOnlyReason === "
|
|
426
|
+
...(current.readOnly ? [t(this.language, current.readOnlyReason === "runtime-catalog" ? "panelModelReadonlyRuntime" : "panelModelReadonly")] : []),
|
|
409
427
|
] : [];
|
|
410
428
|
const rightAll = detailText.flatMap((line) => wrapTextWithAnsi(this.theme.fg("muted", line), rightWidth));
|
|
411
429
|
this.detailMaxScroll = Math.max(0, rightAll.length - bodyBudget);
|
package/src/types.ts
CHANGED
|
@@ -71,6 +71,8 @@ export interface ProviderConnectionConfig {
|
|
|
71
71
|
defaultContextWindow?: number;
|
|
72
72
|
defaultMaxTokens?: number;
|
|
73
73
|
managedChatIds?: string[];
|
|
74
|
+
/** Remote catalog keys (`type\0id`) hidden by the user; retained across discovery refreshes. */
|
|
75
|
+
hiddenRemoteIds?: string[];
|
|
74
76
|
models?: Array<ManagedClassifierModel | ManagedImageModel>;
|
|
75
77
|
modelMetadata?: ModelMetadataEntry[];
|
|
76
78
|
}
|
|
@@ -115,14 +117,15 @@ export interface ProviderDraft {
|
|
|
115
117
|
classifierApi: string;
|
|
116
118
|
imageApi: ImageApiName;
|
|
117
119
|
defaultType: ManagedModelType;
|
|
120
|
+
hiddenRemoteIds?: string[];
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
export type CandidateModel = ManagedModel & {
|
|
121
124
|
source: "managed" | "external" | "installed" | "remote";
|
|
122
125
|
mutableType: boolean;
|
|
123
|
-
/** Read-only entries are visible but cannot be toggled
|
|
126
|
+
/** Read-only entries are visible but cannot be toggled: only runtime catalog rows. */
|
|
124
127
|
readOnly?: boolean;
|
|
125
|
-
readOnlyReason?: "
|
|
128
|
+
readOnlyReason?: "runtime-catalog";
|
|
126
129
|
contextSource?: ModelMetadataSource;
|
|
127
130
|
maxTokensSource?: ModelMetadataSource;
|
|
128
131
|
thinkingSource?: ThinkingMetadataSource;
|
|
@@ -142,6 +145,20 @@ export interface ConfigSnapshot {
|
|
|
142
145
|
text?: string;
|
|
143
146
|
}
|
|
144
147
|
|
|
148
|
+
/** Persistent remote-hide identity. Legacy id-only entries are interpreted as chat-only on read. */
|
|
149
|
+
export function hiddenRemoteKey(model: Pick<ManagedModel, "id" | "type">): string { return `${model.type}\u0000${model.id}`; }
|
|
150
|
+
|
|
151
|
+
export function isHiddenRemote(model: Pick<ManagedModel, "id" | "type">, hiddenKeys: readonly string[]): boolean {
|
|
152
|
+
const key = hiddenRemoteKey(model);
|
|
153
|
+
// Compatibility: prior releases stored bare IDs; treating those as chat-only prevents
|
|
154
|
+
// a legacy hidden chat model from also hiding classifier/image models with the same ID.
|
|
155
|
+
return hiddenKeys.includes(key) || model.type === "chat" && hiddenKeys.includes(model.id);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function normalizeHiddenRemoteKeys(hiddenKeys: readonly string[]): string[] {
|
|
159
|
+
return [...new Set(hiddenKeys.map((key) => key.includes("\u0000") ? key : hiddenRemoteKey({ type: "chat", id: key })))].sort((a, b) => a.localeCompare(b));
|
|
160
|
+
}
|
|
161
|
+
|
|
145
162
|
export function identityOf(model: Pick<ManagedModel, "id" | "type"> & { provider?: string }, provider?: string): ModelIdentity {
|
|
146
163
|
const p = provider ?? model.provider ?? "";
|
|
147
164
|
return `${p}\u0000${model.type}\u0000${model.id}`;
|