@wuyaos/pi-model-config 0.1.7 → 0.1.8
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/README.md +18 -0
- package/model-config.schema.json +5 -0
- package/package.json +1 -1
- package/src/auto-probe.ts +219 -0
- package/src/command.ts +54 -2
- package/src/config.ts +3 -0
- package/src/i18n.ts +8 -0
- package/src/types.ts +2 -0
package/README.md
CHANGED
|
@@ -178,6 +178,24 @@ Chat 写入 Pi 标准 `models.json`。Classifier 和 image 通过 Pi 的原生 m
|
|
|
178
178
|
|
|
179
179
|
插件是与 Pi 同权限运行的扩展,可以读取模型配置和认证。只从可信来源安装;优先使用 `$ENV` 或 Pi auth,避免长期 literal。
|
|
180
180
|
|
|
181
|
+
## 保存时自动探测参数(auto probe on save)
|
|
182
|
+
|
|
183
|
+
保存流程进入预览前(`Ctrl+S` 之后、preview 展示之前),插件会对草稿中 metadata 仍标记为 `compat`(即缺少 contextWindow/maxTokens 元数据的)chat 模型逐个串行发送一个最小探测请求:`POST {baseUrl}/chat/completions`,body 为 `{ model, messages: [{ role: "user", content: "用中文写一段约300字的描写" }], max_tokens: 100000 }`,单请求 15 秒超时。每个请求都是真实调用,可能计费;探测期间显示统一盒式进度面板(已完成 X/Y),Esc 可取消剩余请求,已完成的探测结果保留。
|
|
184
|
+
|
|
185
|
+
推断规则:`finish_reason=length` 且 `usage.completion_tokens_details.reasoning_tokens > 0`(或 `message.reasoning_content` 存在)→ `maxTokens = max(2 × reasoning_tokens, 16384)`,并标 `reasoning=true` + 协议默认 thinkingLevelMap;`finish_reason=length` 无 reasoning → `maxTokens = max(completion_tokens, 16384)`(保守取当前截断点);`finish_reason=stop` → `maxTokens = 32768`;HTTP 错误、超时、取消或不可信响应 → 该模型静默沿用 128K/16K 兼容默认值。contextWindow 无法由探测可靠测出,保留现有值或 128000 默认,但 metadata 也会标记为 `probed` 表示本轮已探测确认。探测成功回填草稿并标 `maxTokensSource/contextSource/thinkingSource="probed"`;最终落盘仍需 preview 确认,preview 的「模型配置」分类会为新增条目显示「参数已自动探测(实测)」来源标记。
|
|
186
|
+
|
|
187
|
+
凭据与 URL 安全复用 discovery 的规则:URL 必须通过 `validateHttpUrl`(仅 HTTP(S)、禁止嵌入凭据或密钥查询参数);`$ENV`/`$$` 字面量按 discovery 同样方式解析;reuse-pi 的 Pi 凭据仅在与模型地址同 origin 时才随探测请求转发。provider 专属代理同样适用于探测请求。探测密钥通过 `Authorization: Bearer` 头发送且不会出现在任何错误提示中。
|
|
188
|
+
|
|
189
|
+
该功能默认开启;在 `model-config.json` 顶层设置 `"autoProbeOnSave": false` 可整体关闭:
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
{
|
|
193
|
+
"version": 1,
|
|
194
|
+
"autoProbeOnSave": false,
|
|
195
|
+
"providers": {}
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
181
199
|
## 从 jev-use 迁移到原生 classifier
|
|
182
200
|
|
|
183
201
|
本插件不会卸载或禁用 `jev-use`。建议手工迁移:
|
package/model-config.schema.json
CHANGED
|
@@ -14,6 +14,11 @@
|
|
|
14
14
|
"version": {
|
|
15
15
|
"const": 1
|
|
16
16
|
},
|
|
17
|
+
"autoProbeOnSave": {
|
|
18
|
+
"description": "Pre-save minimal parameter probe for newly checked chat models lacking context/maxTokens metadata. Default true.",
|
|
19
|
+
"type": "boolean",
|
|
20
|
+
"default": true
|
|
21
|
+
},
|
|
17
22
|
"providers": {
|
|
18
23
|
"type": "object",
|
|
19
24
|
"additionalProperties": {
|
package/package.json
CHANGED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import type { ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Key, matchesKey, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
3
|
+
import { validateHttpUrl } from "./config.ts";
|
|
4
|
+
import { MODEL_CONFIG_OVERLAY_OPTIONS, renderPanelBox } from "./panel.ts";
|
|
5
|
+
import { t, type UiLanguage } from "./i18n.ts";
|
|
6
|
+
import { protocolThinkingMap } from "./thinking-defaults.ts";
|
|
7
|
+
import type { ProviderDraft, ThinkingLevelMap } from "./types.ts";
|
|
8
|
+
|
|
9
|
+
/** Defaults applied when the minimal probe request fails or cannot be trusted. */
|
|
10
|
+
export const AUTO_PROBE_TIMEOUT_MS = 15_000;
|
|
11
|
+
export const AUTO_PROBE_MAX_TOKENS_REQUEST = 100_000;
|
|
12
|
+
const PROBE_PROMPT = "用中文写一段约300字的描写";
|
|
13
|
+
|
|
14
|
+
export interface AutoProbeResult {
|
|
15
|
+
/** Inferred output ceiling. Context window is intentionally not probed. */
|
|
16
|
+
maxTokens: number;
|
|
17
|
+
reasoning: boolean;
|
|
18
|
+
thinkingLevelMap?: ThinkingLevelMap;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ProbeChatParamsOptions {
|
|
22
|
+
fetch?: typeof fetch;
|
|
23
|
+
apiKey?: string;
|
|
24
|
+
baseUrl: string;
|
|
25
|
+
signal: AbortSignal;
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Pure inference from a minimal chat-completions response. Returns null whenever the
|
|
31
|
+
* observation is not trustworthy, so callers can fall back to compatibility defaults.
|
|
32
|
+
*/
|
|
33
|
+
export function inferAutoProbeParams(
|
|
34
|
+
payload: {
|
|
35
|
+
finishReason?: unknown;
|
|
36
|
+
reasoningContent?: unknown;
|
|
37
|
+
reasoningTokens?: unknown;
|
|
38
|
+
completionTokens?: unknown;
|
|
39
|
+
},
|
|
40
|
+
api: string,
|
|
41
|
+
): AutoProbeResult | null {
|
|
42
|
+
const reasoningTokens = typeof payload.reasoningTokens === "number" && Number.isFinite(payload.reasoningTokens) && payload.reasoningTokens > 0
|
|
43
|
+
? Math.floor(payload.reasoningTokens)
|
|
44
|
+
: 0;
|
|
45
|
+
const hasReasoningContent = typeof payload.reasoningContent === "string" && payload.reasoningContent.trim().length > 0;
|
|
46
|
+
const reasoning = hasReasoningContent || reasoningTokens > 0;
|
|
47
|
+
const finish = payload.finishReason;
|
|
48
|
+
if (finish === "length") {
|
|
49
|
+
// Truncated mid-reasoning: the model wanted at least its reasoning budget twice.
|
|
50
|
+
const maxTokens = reasoningTokens > 0 ? Math.max(2 * reasoningTokens, 16_384) : Math.max(Math.floor(Number(payload.completionTokens)) || 0, 16_384);
|
|
51
|
+
const protocol = reasoning ? protocolThinkingMap(api) : undefined;
|
|
52
|
+
return { maxTokens, reasoning, ...(protocol ? { thinkingLevelMap: protocol } : {}) };
|
|
53
|
+
}
|
|
54
|
+
if (finish === "stop") return { maxTokens: 32_768, reasoning };
|
|
55
|
+
// tool_calls / content_filter / unknown shapes are not trustworthy for sizing.
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* One minimal paid-capable request against an OpenAI-compatible /chat/completions
|
|
61
|
+
* endpoint. Never throws: every failure path resolves to null so the save flow can
|
|
62
|
+
* silently keep compatibility defaults. URL handling reuses validateHttpUrl, matching
|
|
63
|
+
* discovery SSRF rules (HTTP(S) only, no embedded credentials or secret queries).
|
|
64
|
+
*/
|
|
65
|
+
export async function probeChatParams(model: { id: string; api: string }, options: ProbeChatParamsOptions): Promise<AutoProbeResult | null> {
|
|
66
|
+
try {
|
|
67
|
+
const base = validateHttpUrl(options.baseUrl, "Base URL").replace(/\/+$/u, "");
|
|
68
|
+
const url = validateHttpUrl(`${base}/chat/completions`, "Probe URL");
|
|
69
|
+
const headers: Record<string, string> = { "content-type": "application/json" };
|
|
70
|
+
if (options.apiKey) headers.authorization = `Bearer ${options.apiKey}`;
|
|
71
|
+
const combined = AbortSignal.any([options.signal, AbortSignal.timeout(options.timeoutMs ?? AUTO_PROBE_TIMEOUT_MS)]);
|
|
72
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
73
|
+
const response = await fetchImpl(url, {
|
|
74
|
+
method: "POST",
|
|
75
|
+
headers,
|
|
76
|
+
body: JSON.stringify({
|
|
77
|
+
model: model.id,
|
|
78
|
+
messages: [{ role: "user", content: PROBE_PROMPT }],
|
|
79
|
+
max_tokens: AUTO_PROBE_MAX_TOKENS_REQUEST,
|
|
80
|
+
}),
|
|
81
|
+
signal: combined,
|
|
82
|
+
});
|
|
83
|
+
if (!response.ok) return null;
|
|
84
|
+
const text = await response.text();
|
|
85
|
+
let body: any;
|
|
86
|
+
try { body = JSON.parse(text); } catch { return null; }
|
|
87
|
+
const choice = Array.isArray(body?.choices) ? body.choices[0] : undefined;
|
|
88
|
+
if (!choice || typeof choice !== "object") return null;
|
|
89
|
+
return inferAutoProbeParams({
|
|
90
|
+
finishReason: choice.finish_reason,
|
|
91
|
+
reasoningContent: choice.message?.reasoning_content,
|
|
92
|
+
reasoningTokens: body?.usage?.completion_tokens_details?.reasoning_tokens,
|
|
93
|
+
completionTokens: body?.usage?.completion_tokens,
|
|
94
|
+
}, model.api);
|
|
95
|
+
} catch {
|
|
96
|
+
// Abort (user Esc or timeout) and any transport/validation failure fall back to defaults.
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function originOf(value: string | undefined): string | undefined {
|
|
102
|
+
if (!value) return undefined;
|
|
103
|
+
try { return new URL(validateHttpUrl(value, "Base URL")).origin; } catch { return undefined; }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Resolve the credential used by the probe request, applying the same origin
|
|
108
|
+
* containment as discovery for reused Pi credentials: an existing key is never
|
|
109
|
+
* forwarded to a different origin than the one it was issued for.
|
|
110
|
+
*/
|
|
111
|
+
export function resolveProbeApiKey(
|
|
112
|
+
draft: Pick<ProviderDraft, "authMode" | "apiKeyRaw" | "temporaryKey">,
|
|
113
|
+
piAuth: { auth?: { apiKey?: string; baseUrl?: string } } | undefined,
|
|
114
|
+
baseUrl: string,
|
|
115
|
+
): string | undefined {
|
|
116
|
+
if (draft.authMode === "models-json") {
|
|
117
|
+
const raw = draft.apiKeyRaw;
|
|
118
|
+
const envMatch = raw?.match(/^\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))$/u);
|
|
119
|
+
if (envMatch) return process.env[envMatch[1] ?? envMatch[2]!];
|
|
120
|
+
if (raw?.startsWith("$$") || raw?.startsWith("$!")) return raw.slice(1);
|
|
121
|
+
return raw;
|
|
122
|
+
}
|
|
123
|
+
if (draft.authMode === "temporary") return draft.temporaryKey;
|
|
124
|
+
if (draft.authMode === "reuse-pi") {
|
|
125
|
+
const key = piAuth?.auth?.apiKey;
|
|
126
|
+
if (!key) return undefined;
|
|
127
|
+
const authOrigin = originOf(piAuth?.auth?.baseUrl);
|
|
128
|
+
const modelOrigin = originOf(baseUrl);
|
|
129
|
+
if (!authOrigin || !modelOrigin || authOrigin !== modelOrigin) return undefined;
|
|
130
|
+
return key;
|
|
131
|
+
}
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Chat models whose metadata still marks context/maxTokens as unverified compat defaults. */
|
|
136
|
+
export function probeTargetsFor(
|
|
137
|
+
models: ReadonlyArray<{ type: string; id: string }>,
|
|
138
|
+
metadata: ReadonlyArray<{ type: string; id: string; contextSource?: string; maxTokensSource?: string }>,
|
|
139
|
+
): Array<{ type: string; id: string }> {
|
|
140
|
+
return models.filter((model) => {
|
|
141
|
+
if (model.type !== "chat") return false;
|
|
142
|
+
const entry = metadata.find((item) => item.type === model.type && item.id === model.id);
|
|
143
|
+
return !entry || entry.contextSource === "compat" || entry.maxTokensSource === "compat";
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface ProbeQueueEntry<T> {
|
|
148
|
+
label: string;
|
|
149
|
+
run: (signal: AbortSignal) => Promise<T | null>;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface ProbeQueueOutcome<T> {
|
|
153
|
+
result: T | null;
|
|
154
|
+
/** True when this entry was skipped because the user pressed Esc earlier. */
|
|
155
|
+
cancelled: boolean;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Serial runner shared by the TUI panel and tests; one request at a time, abort stops the rest. */
|
|
159
|
+
export async function runProbeQueue<T>(
|
|
160
|
+
entries: ReadonlyArray<ProbeQueueEntry<T>>,
|
|
161
|
+
signal: AbortSignal,
|
|
162
|
+
onProgress?: (done: number, total: number, current: string) => void,
|
|
163
|
+
): Promise<Array<ProbeQueueOutcome<T>>> {
|
|
164
|
+
const outcomes: Array<ProbeQueueOutcome<T>> = [];
|
|
165
|
+
for (const entry of entries) {
|
|
166
|
+
if (signal.aborted) { outcomes.push({ result: null, cancelled: true }); continue; }
|
|
167
|
+
onProgress?.(outcomes.length, entries.length, entry.label);
|
|
168
|
+
const result = await entry.run(signal);
|
|
169
|
+
outcomes.push({ result, cancelled: false });
|
|
170
|
+
onProgress?.(outcomes.length, entries.length, entry.label);
|
|
171
|
+
}
|
|
172
|
+
return outcomes;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
class AutoProbePanel<T> implements Component {
|
|
176
|
+
private controller = new AbortController();
|
|
177
|
+
private done = 0;
|
|
178
|
+
private current = "";
|
|
179
|
+
constructor(
|
|
180
|
+
private tui: TUI,
|
|
181
|
+
private theme: Theme,
|
|
182
|
+
private language: UiLanguage,
|
|
183
|
+
private entries: ReadonlyArray<ProbeQueueEntry<T>>,
|
|
184
|
+
done: (outcomes: Array<ProbeQueueOutcome<T>>) => void,
|
|
185
|
+
) {
|
|
186
|
+
void runProbeQueue(this.entries, this.controller.signal, (completed, _total, label) => {
|
|
187
|
+
this.done = completed;
|
|
188
|
+
this.current = label;
|
|
189
|
+
this.tui.requestRender();
|
|
190
|
+
}).then((outcomes) => {
|
|
191
|
+
this.done = outcomes.length;
|
|
192
|
+
this.current = "";
|
|
193
|
+
this.tui.requestRender();
|
|
194
|
+
done(outcomes);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
handleInput(data: string): void { if (matchesKey(data, Key.escape)) this.controller.abort(new DOMException("Cancelled", "AbortError")); }
|
|
198
|
+
render(width: number): string[] {
|
|
199
|
+
const lines = [t(this.language, "autoProbeProgress", { done: this.done, total: this.entries.length, model: this.current })];
|
|
200
|
+
return renderPanelBox(lines, width, t(this.language, "autoProbePanelTitle"), this.theme);
|
|
201
|
+
}
|
|
202
|
+
invalidate(): void {}
|
|
203
|
+
dispose(): void { this.controller.abort(); }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Unified boxed progress panel for the pre-save auto probe. Shows X/Y completed and
|
|
208
|
+
* lets Esc cancel the remaining (not in-flight) requests; completed results are kept.
|
|
209
|
+
*/
|
|
210
|
+
export async function runAutoProbeFlow<T>(
|
|
211
|
+
ctx: ExtensionCommandContext,
|
|
212
|
+
language: UiLanguage,
|
|
213
|
+
entries: ReadonlyArray<ProbeQueueEntry<T>>,
|
|
214
|
+
): Promise<Array<ProbeQueueOutcome<T>>> {
|
|
215
|
+
return ctx.ui.custom<Array<ProbeQueueOutcome<T>>>(
|
|
216
|
+
(tui, theme, _keys, done) => new AutoProbePanel(tui, theme, language, entries, done),
|
|
217
|
+
MODEL_CONFIG_OVERLAY_OPTIONS,
|
|
218
|
+
);
|
|
219
|
+
}
|
package/src/command.ts
CHANGED
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
writeConfigTransaction,
|
|
13
13
|
} from "./config.ts";
|
|
14
14
|
import { buildDiscoveryCandidates, discoverModelsFromCandidates } from "./discovery.ts";
|
|
15
|
+
import { createProviderFetch } from "./transport.ts";
|
|
16
|
+
import { probeChatParams, probeTargetsFor, resolveProbeApiKey, runAutoProbeFlow } from "./auto-probe.ts";
|
|
15
17
|
import { formatError, mcError, t, type UiLanguage } from "./i18n.ts";
|
|
16
18
|
import {
|
|
17
19
|
showConnectionPanel,
|
|
@@ -457,6 +459,7 @@ function buildPreviewDetails(
|
|
|
457
459
|
oldConfig: ProviderConnectionConfig | undefined,
|
|
458
460
|
oldProvider: ModelsJsonProvider | undefined,
|
|
459
461
|
draft: ProviderDraft,
|
|
462
|
+
metadata: ReadonlyArray<ModelMetadataEntry> = [],
|
|
460
463
|
): { providerDetails: string[]; modelDetails: string[] } {
|
|
461
464
|
const oldMap = new Map(before.map((model) => [identityOf(model, providerId), model]));
|
|
462
465
|
const newMap = new Map(after.map((model) => [identityOf(model, providerId), model]));
|
|
@@ -497,6 +500,13 @@ function buildPreviewDetails(
|
|
|
497
500
|
const model = newMap.get(key) ?? oldMap.get(key)!;
|
|
498
501
|
return `${modelTypeLabel(language, model.type)}/${model.id}`;
|
|
499
502
|
};
|
|
503
|
+
// Newly added rows whose metadata was just probed get an explicit source marker.
|
|
504
|
+
const describeAdded = (key: ReturnType<typeof identityOf>) => {
|
|
505
|
+
const model = newMap.get(key)!;
|
|
506
|
+
const entry = metadata.find((item) => item.type === model.type && item.id === model.id);
|
|
507
|
+
const probed = entry?.contextSource === "probed" || entry?.maxTokensSource === "probed";
|
|
508
|
+
return `${describe(key)}${probed ? ` · ${t(language, "previewProbedDefaults")}` : ""}`;
|
|
509
|
+
};
|
|
500
510
|
const settingsChanged = oldConnection !== nextConnection;
|
|
501
511
|
const keyWillUpdate = draft.authMode === "models-json" && oldProvider?.apiKey !== draft.apiKeyRaw;
|
|
502
512
|
return {
|
|
@@ -506,7 +516,7 @@ function buildPreviewDetails(
|
|
|
506
516
|
...(keyWillUpdate ? [t(language, "previewKeyUpdated")] : []),
|
|
507
517
|
],
|
|
508
518
|
modelDetails: [
|
|
509
|
-
...(added.length ? [t(language, "previewModelsAdded", { items: added.map(
|
|
519
|
+
...(added.length ? [t(language, "previewModelsAdded", { items: added.map(describeAdded).join(", ") })] : []),
|
|
510
520
|
...(deleted.length ? [t(language, "previewModelsRemoved", { items: deleted.map(describe).join(", ") })] : []),
|
|
511
521
|
...(modified.length ? [t(language, "previewModelsModified", { items: modified.map(describe).join(", ") })] : []),
|
|
512
522
|
t(language, "previewModelsUnchanged", { count: unchanged }),
|
|
@@ -882,6 +892,48 @@ export function registerModelConfigCommand(pi: ExtensionAPI, startupIssues: Star
|
|
|
882
892
|
});
|
|
883
893
|
if (empty !== "confirm") return false;
|
|
884
894
|
}
|
|
895
|
+
if (files.config.autoProbeOnSave !== false && draft.enabled) {
|
|
896
|
+
const wanted = new Set(probeTargetsFor(selectedDraft, selectedMetadata).map((model) => `${model.type}\u0000${model.id}`));
|
|
897
|
+
const targets = selectedDraft
|
|
898
|
+
.map((model, index) => ({ model, index, metadataIndex: selectedMetadata.findIndex((entry) => entry.type === model.type && entry.id === model.id) }))
|
|
899
|
+
.filter(({ model }) => wanted.has(`${model.type}\u0000${model.id}`));
|
|
900
|
+
if (targets.length) {
|
|
901
|
+
try {
|
|
902
|
+
const piAuth = draft.authMode === "reuse-pi" ? await ctx.modelRegistry.getProviderAuth(target.id) : undefined;
|
|
903
|
+
const fetchImpl = draft.proxyUrl ? createProviderFetch(draft.proxyUrl) : globalThis.fetch;
|
|
904
|
+
const entries = targets.map(({ model }) => ({
|
|
905
|
+
label: model.id,
|
|
906
|
+
run: (signal: AbortSignal) => {
|
|
907
|
+
const baseUrl = model.type === "chat" ? model.baseUrl ?? draft.baseUrl : draft.baseUrl;
|
|
908
|
+
if (!baseUrl) return Promise.resolve(null);
|
|
909
|
+
return probeChatParams(model, { fetch: fetchImpl, apiKey: resolveProbeApiKey(draft, piAuth, baseUrl), baseUrl, signal });
|
|
910
|
+
},
|
|
911
|
+
}));
|
|
912
|
+
const outcomes = await runAutoProbeFlow(ctx, language, entries);
|
|
913
|
+
for (let index = 0; index < targets.length; index++) {
|
|
914
|
+
const { model, metadataIndex } = targets[index]!;
|
|
915
|
+
const outcome = outcomes[index]!;
|
|
916
|
+
if (model.type !== "chat" || outcome.cancelled || !outcome.result) continue;
|
|
917
|
+
const result = outcome.result;
|
|
918
|
+
model.maxTokens = result.maxTokens;
|
|
919
|
+
model.reasoning = result.reasoning;
|
|
920
|
+
if (result.reasoning && result.thinkingLevelMap) model.thinkingLevelMap = { ...result.thinkingLevelMap };
|
|
921
|
+
else delete model.thinkingLevelMap;
|
|
922
|
+
const metadata = {
|
|
923
|
+
...(metadataIndex >= 0 ? selectedMetadata[metadataIndex] : { type: model.type, id: model.id }),
|
|
924
|
+
contextSource: "probed" as const,
|
|
925
|
+
maxTokensSource: "probed" as const,
|
|
926
|
+
thinkingSource: "probed" as const,
|
|
927
|
+
verifiedAt: new Date().toISOString(),
|
|
928
|
+
};
|
|
929
|
+
if (metadataIndex >= 0) selectedMetadata[metadataIndex] = metadata; else selectedMetadata.push(metadata);
|
|
930
|
+
}
|
|
931
|
+
if (outcomes.some((outcome) => outcome.cancelled)) ctx.ui.notify(t(language, "autoProbeCancelled"), "info");
|
|
932
|
+
} catch {
|
|
933
|
+
// Probe failures are silent: unprobed models keep compatibility defaults.
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
885
937
|
return true;
|
|
886
938
|
};
|
|
887
939
|
const adoptExisting = async (): Promise<boolean> => {
|
|
@@ -1053,7 +1105,7 @@ export function registerModelConfigCommand(pi: ExtensionAPI, startupIssues: Star
|
|
|
1053
1105
|
continue;
|
|
1054
1106
|
}
|
|
1055
1107
|
|
|
1056
|
-
const details = buildPreviewDetails(language, target.id, managedBefore, selectedDraft, oldConfig, oldProvider, draft);
|
|
1108
|
+
const details = buildPreviewDetails(language, target.id, managedBefore, selectedDraft, oldConfig, oldProvider, draft, selectedMetadata);
|
|
1057
1109
|
const preview = await showPreviewPanel(ctx, language, target.id, details.providerDetails, details.modelDetails, previewState);
|
|
1058
1110
|
if (preview.action === "back") { page = previewOrigin; continue; }
|
|
1059
1111
|
if (preview.action === "discard") {
|
package/src/config.ts
CHANGED
|
@@ -60,6 +60,9 @@ export function parseModelConfig(source: ConfigSnapshot): ModelConfigFile {
|
|
|
60
60
|
if (!isRecord(parsed) || parsed.version !== 1 || !isRecord(parsed.providers)) {
|
|
61
61
|
throw mcError("MCFG_E_CONFIG_SHAPE", { file: "model-config.json" });
|
|
62
62
|
}
|
|
63
|
+
if (parsed.autoProbeOnSave !== undefined && typeof parsed.autoProbeOnSave !== "boolean") {
|
|
64
|
+
throw mcError("MCFG_E_CONFIG_SHAPE", { file: "model-config.json" });
|
|
65
|
+
}
|
|
63
66
|
for (const [id, provider] of Object.entries(parsed.providers)) {
|
|
64
67
|
if (!isRecord(provider)) throw mcError("MCFG_E_PROVIDER_SHAPE", { file: "model-config.json", provider: id });
|
|
65
68
|
if (!new Set(["models-json", "reuse-pi", "anonymous"]).has(String(provider.authMode))) {
|
package/src/i18n.ts
CHANGED
|
@@ -305,6 +305,10 @@ const ZH = {
|
|
|
305
305
|
previewSettingsUnchanged: "服务商设置无更改",
|
|
306
306
|
previewKeyUpdated: "API 密钥:将更新,内容已隐藏",
|
|
307
307
|
previewModelsAdded: "添加模型:{items}",
|
|
308
|
+
autoProbePanelTitle: "保存前参数探测",
|
|
309
|
+
autoProbeProgress: "探测 {done}/{total}:{model};Esc 取消剩余(已完成项保留)",
|
|
310
|
+
autoProbeCancelled: "已取消自动探测;剩余模型沿用兼容默认值(128K/16K)",
|
|
311
|
+
previewProbedDefaults: "参数已自动探测(实测)",
|
|
308
312
|
previewModelsRemoved: "移除模型:{items}",
|
|
309
313
|
previewModelsModified: "修改模型:{items}",
|
|
310
314
|
previewModelsUnchanged: "未变化模型:{count}",
|
|
@@ -648,6 +652,10 @@ const EN = {
|
|
|
648
652
|
previewSettingsUnchanged: "Provider settings unchanged",
|
|
649
653
|
previewKeyUpdated: "API key: will be updated; content is hidden",
|
|
650
654
|
previewModelsAdded: "Add models: {items}",
|
|
655
|
+
autoProbePanelTitle: "Pre-save parameter probe",
|
|
656
|
+
autoProbeProgress: "Probing {done}/{total}: {model}; Esc cancels the rest (completed results are kept)",
|
|
657
|
+
autoProbeCancelled: "Auto-probe cancelled; remaining models keep compatibility defaults (128K/16K)",
|
|
658
|
+
previewProbedDefaults: "parameters auto-probed",
|
|
651
659
|
previewModelsRemoved: "Remove models: {items}",
|
|
652
660
|
previewModelsModified: "Modify models: {items}",
|
|
653
661
|
previewModelsUnchanged: "Unchanged models: {count}",
|
package/src/types.ts
CHANGED
|
@@ -80,6 +80,8 @@ export interface ProviderConnectionConfig {
|
|
|
80
80
|
export interface ModelConfigFile {
|
|
81
81
|
$schema?: string;
|
|
82
82
|
version: 1;
|
|
83
|
+
/** Pre-save minimal parameter probe for newly checked chat models lacking context/max metadata; default true. */
|
|
84
|
+
autoProbeOnSave?: boolean;
|
|
83
85
|
providers: Record<string, ProviderConnectionConfig>;
|
|
84
86
|
[key: string]: unknown;
|
|
85
87
|
}
|