/** * Model meta override dialog using Pi native select/input/confirm. * Avoids the cramped three-column TUI for multi-field editing. * * Editing model: * - scope: provider (all models) or one model id * - every field is an enum/quick-pick, never free text (thinkingFormat, * reasoning tri-state, contextWindow/maxTokens presets + custom) * - each row shows 覆写 / 继承 / 内置 / 默认 so inheritance is visible * - unsaved edits are marked and confirmed before discard */ import type { CcProvider } from "../types.ts"; import { THINKING_FORMATS } from "../types.ts"; import type { ModelMetaOverride } from "../types.ts"; import type { TrustedMaxTokensHint } from "../capabilities/resolve.ts"; import type { ThinkingProjectionDecision } from "../capabilities/thinking-projection.ts"; import { activeExactModelThinkingOptIn, toggleExactModelThinkingOptIn, type ExactModelThinkingOptInRequest, } from "../capabilities/thinking-opt-in.ts"; import { cleanModelMeta, inheritedModelMetaBelowExact, mergeModelMeta, matchExactModelOverride, matchModelOverride, MODEL_META_PRESETS, type ModelMetaField, type ModelMetaPreset, } from "../model-meta.ts"; import { builtInCompatForModelId } from "../compat/built-in-compat-profile.ts"; import { maxTokensHintForScope, shouldShowBuiltInCompatRow, syncValueLabel, thinkingOptInForScope, useBuiltInCompatStateText, userMetaForBuiltInGate, } from "./model-meta-form.ts"; export type ModelMetaDialogUi = { select: (title: string, options: string[]) => Promise; input: (prompt: string, defaultValue?: string) => Promise; confirm: (message: string) => Promise; notify?: (message: string, level?: "info" | "warning" | "error" | string) => void; }; /** Which layer an edit writes to. */ export type ModelMetaScope = | { kind: "provider" } | { kind: "model"; modelId: string }; export interface ModelMetaDialogInput { provider: Pick; /** Scope the dialog opens in. */ scope: ModelMetaScope; /** Explicit provider-scope override on disk. */ providerMeta?: ModelMetaOverride; /** Explicit per-model overrides on disk (keys may be globs). */ modelOverrides?: Record; /** config.defaultModelMeta */ base?: ModelMetaOverride; /** Protocol-tier fallback, shown as 默认 when no layer sets a field. */ tier?: ModelMetaOverride; /** Exact-model maxTokens sync hints (trusted only: models.dev / CC Switch meta). */ maxTokensHints?: Record; /** Tuple-scoped registration decisions; only exact-model entries are offered. */ thinkingProjections?: Record; /** Model ids offered when switching scope. */ models?: string[]; } export type ModelMetaDialogResult = | { kind: "save"; scope: ModelMetaScope; modelMeta: ModelMetaOverride; thinkingOptIn?: ExactModelThinkingOptInRequest; } | { kind: "clear"; scope: ModelMetaScope } | { kind: "clearAll" } | { kind: "cancel" }; // User-requested commonly-used context windows; `自定义` covers the rest. const CONTEXT_PRESETS = [200_000, 256_000, 500_000, 1_000_000]; const MAX_TOKENS_PRESETS = [4_096, 8_192, 16_000, 32_000, 64_000, 128_000]; const BACK = "← 返回"; /** Compact human form: 128000 → 128k, 1000000 → 1M, else exact digits. */ export function formatCount(n: number): string { if (n % 1_000_000 === 0) return `${n / 1_000_000}M`; if (n % 1_000 === 0) return `${n / 1_000}k`; return String(n); } /** * Accept 200000 / 200k / 200K / 1m / 1_000_000 / 1,000,000. * Returns undefined for empty, NaN for invalid. */ export function parseCount(raw: string): number | undefined | typeof NaN { const t = raw.trim().replace(/[_,\s]/g, ""); if (!t) return undefined; const m = /^(\d+(?:\.\d+)?)([kKmM])?$/.exec(t); if (!m) return NaN; const value = Number(m[1]); if (!Number.isFinite(value) || value <= 0) return NaN; const mult = m[2] ? (m[2].toLowerCase() === "k" ? 1_000 : 1_000_000) : 1; const n = Math.floor(value * mult); return n > 0 ? n : NaN; } function fmtFieldValue( field: ModelMetaField, value: boolean | number | string, ): string { if (typeof value === "number") return formatCount(value); return String(value); } function scopeLabel(scope: ModelMetaScope): string { return scope.kind === "provider" ? "全部模型" : `模型 ${scope.modelId}`; } function sameMeta(a: ModelMetaOverride | undefined, b: ModelMetaOverride | undefined): boolean { const norm = (m: ModelMetaOverride | undefined) => { const c = cleanModelMeta(m) ?? {}; return JSON.stringify( Object.keys(c) .sort() .map((k) => [k, (c as Record)[k]]), ); }; return norm(a) === norm(b); } function applyPreset(draft: ModelMetaOverride, preset: ModelMetaPreset): void { Object.assign(draft, cleanModelMeta(preset.modelMeta) ?? {}); } /** * Interactive loop to edit modelMeta for one provider or one model. * Returns save/clear/clearAll/cancel; caller persists. */ export async function runModelMetaDialog( ui: ModelMetaDialogUi, input: ModelMetaDialogInput, ): Promise { const { provider } = input; const modelOverrides = input.modelOverrides ?? {}; let scope: ModelMetaScope = input.scope; /** Explicit override stored at the current scope. */ const storedFor = (s: ModelMetaScope): ModelMetaOverride | undefined => s.kind === "provider" ? cleanModelMeta(input.providerMeta) : matchExactModelOverride(modelOverrides, s.modelId)?.modelMeta; /** Layers below the current scope (model: base ⊕ provider ⊕ glob-only). */ const inheritedFor = (s: ModelMetaScope): ModelMetaOverride | undefined => s.kind === "provider" ? cleanModelMeta(input.base) : inheritedModelMetaBelowExact( input.base, input.providerMeta, modelOverrides, s.modelId, ); /** Built-in compat for model scope; honors draft/inherited opt-out. */ const builtInFor = ( s: ModelMetaScope, userMeta?: ModelMetaOverride, ): ModelMetaOverride | undefined => s.kind === "model" ? builtInCompatForModelId(s.modelId, userMeta) : undefined; let stored = storedFor(scope); let draft: ModelMetaOverride = { ...(stored ?? {}) }; let requestedThinkingOptIn: ExactModelThinkingOptInRequest | undefined; const dirty = (): boolean => !sameMeta(draft, stored); const hasAnyOverride = (): boolean => Boolean(cleanModelMeta(input.providerMeta)) || Object.keys(modelOverrides).length > 0; const confirmDiscard = async (): Promise => !dirty() || (await ui.confirm("有未保存的修改,放弃并退出?")); /** Row label for one field: 覆写 / 继承 / 内置 / 默认. */ const fieldRow = ( field: ModelMetaField, inherited: ModelMetaOverride | undefined, builtIn?: ModelMetaOverride, ): string => { const own = draft[field]; if (own !== undefined) return `${field} · 覆写 ${fmtFieldValue(field, own)}`; const inh = inherited?.[field]; if (inh !== undefined) return `${field} · 继承 ${fmtFieldValue(field, inh)}`; const bi = builtIn?.[field]; if (bi !== undefined) return `${field} · 内置 ${fmtFieldValue(field, bi)}`; const tier = input.tier?.[field]; if (tier !== undefined) return `${field} · 默认 ${fmtFieldValue(field, tier)}`; return `${field} · 默认`; }; async function pickScope(): Promise { if (!(await confirmDiscard())) return; const rows: { label: string; scope: ModelMetaScope }[] = [ { label: `全部模型(provider 级)${cleanModelMeta(input.providerMeta) ? " ⚙" : ""}`, scope: { kind: "provider" }, }, ]; for (const id of input.models ?? []) { const hit = matchModelOverride(modelOverrides, id); rows.push({ label: `${id}${hit?.modelMeta ? " ⚙" : ""}`, scope: { kind: "model", modelId: id } }); } const MANUAL = "✎ 手动输入 model id / glob"; const labels = [...rows.map((r) => r.label), MANUAL, BACK]; const pick = await ui.select(`作用域 · ${provider.displayName}`, labels); if (!pick || pick === BACK) return; let next: ModelMetaScope | undefined; if (pick === MANUAL) { const raw = await ui.input("model id 或 glob(如 gpt-5* / *sonnet*)", ""); const id = raw?.trim(); if (!id) return; next = { kind: "model", modelId: id }; } else { next = rows[labels.indexOf(pick)]?.scope; } if (!next) return; scope = next; stored = storedFor(scope); draft = { ...(stored ?? {}) }; requestedThinkingOptIn = undefined; } async function pickPreset(): Promise { const labels = [ ...MODEL_META_PRESETS.map((p) => `${p.label} · ${p.description}`), BACK, ]; const pick = await ui.select("预设", labels); if (!pick || pick === BACK) return; const hit = MODEL_META_PRESETS[labels.indexOf(pick)]; if (!hit) return; applyPreset(draft, hit); ui.notify?.(`已应用预设:${hit.label}(未保存)`, "info"); } async function pickReasoning(inherited: ModelMetaOverride | undefined): Promise { const inheritedValue = inherited?.reasoning ?? input.tier?.reasoning; const inheritLabel = inheritedValue === undefined ? "不覆写(继承默认)" : `不覆写(继承 ${inheritedValue})`; const labels = [inheritLabel, "true · 允许 reasoning/thinking", "false · 中转拒收时用", BACK]; const pick = await ui.select("reasoning", labels); if (!pick || pick === BACK) return; if (pick === inheritLabel) delete draft.reasoning; else draft.reasoning = pick.startsWith("true"); } async function pickCount( field: "contextWindow" | "maxTokens", inherited: ModelMetaOverride | undefined, ): Promise { const presets = field === "contextWindow" ? CONTEXT_PRESETS : MAX_TOKENS_PRESETS; const inheritedValue = inherited?.[field] ?? input.tier?.[field]; const inheritLabel = inheritedValue === undefined ? "不覆写(继承默认)" : `不覆写(继承 ${formatCount(inheritedValue as number)})`; const CUSTOM = "✎ 自定义…"; // Trusted sync value offered as a one-key pin at exact-model scope only. const hint = field === "maxTokens" ? maxTokensHintForScope(input, scope) : undefined; // Avoid "4096 · 4096": show exact value only when compact form differs. const presetLabel = (n: number): string => { const compact = formatCount(n); return compact === String(n) ? compact : `${compact} · ${n}`; }; // Label → value, so adding a row can never shift a preset's meaning. const byLabel = new Map(); if (hint) byLabel.set(syncValueLabel(hint), hint.value); for (const n of presets) byLabel.set(presetLabel(n), n); const labels = [...byLabel.keys(), CUSTOM, inheritLabel, BACK]; const pick = await ui.select(field, labels); if (!pick || pick === BACK) return; if (pick === inheritLabel) { delete draft[field]; return; } if (pick === CUSTOM) { const raw = await ui.input( `${field}(支持 200k / 1M / 200000;空=取消)`, draft[field] != null ? String(draft[field]) : "", ); if (raw == null) return; const n = parseCount(raw); if (n === undefined) return; if (Number.isNaN(n)) { ui.notify?.("请输入正整数,可带 k/M 后缀", "warning"); return; } draft[field] = n as number; return; } const value = byLabel.get(pick); if (typeof value === "number") draft[field] = value; } async function pickThinkingFormat( inherited: ModelMetaOverride | undefined, builtIn?: ModelMetaOverride, ): Promise { const userValue = inherited?.thinkingFormat; const builtInValue = builtIn?.thinkingFormat; let inheritLabel: string; if (userValue !== undefined) inheritLabel = `不覆写(继承 ${userValue})`; else if (builtInValue !== undefined) inheritLabel = `不覆写(内置 ${builtInValue})`; else inheritLabel = "不覆写(继承默认)"; const labels = [inheritLabel, ...THINKING_FORMATS, BACK]; const pick = await ui.select("thinkingFormat", labels); if (!pick || pick === BACK) return; if (pick === inheritLabel) { delete draft.thinkingFormat; return; } draft.thinkingFormat = pick; } async function pickUseBuiltInCompat( inherited: ModelMetaOverride | undefined, ): Promise { const state = useBuiltInCompatStateText(draft, inherited); const inheritHint = typeof inherited?.useBuiltInCompat === "boolean" ? `不覆写(继承 ${inherited.useBuiltInCompat ? "开启" : "关闭"})` : "不覆写(默认 开启)"; const labels = [inheritHint, "关闭(禁用内置 profile)", "开启(显式启用)", BACK]; const pick = await ui.select(`内置compat · 当前 ${state}`, labels); if (!pick || pick === BACK) return; if (pick === inheritHint) delete draft.useBuiltInCompat; else if (pick.startsWith("关闭")) draft.useBuiltInCompat = false; else if (pick.startsWith("开启")) draft.useBuiltInCompat = true; } while (true) { const inherited = inheritedFor(scope); const gate = userMetaForBuiltInGate(draft, inherited); const builtIn = builtInFor(scope, gate); const title = `参数覆写 · ${provider.displayName} · ${scopeLabel(scope)}${dirty() ? " ✱" : ""}`; const SCOPE_ROW = `作用域 · ${scopeLabel(scope)}`; const PRESET_ROW = "预设 · 中转兼容 / 完整推理"; const REASONING_ROW = fieldRow("reasoning", inherited, builtIn); const CONTEXT_ROW = fieldRow("contextWindow", inherited, builtIn); const MAXTOKENS_ROW = fieldRow("maxTokens", inherited, builtIn); const THINKING_ROW = fieldRow("thinkingFormat", inherited, builtIn); const thinkingOptIn = thinkingOptInForScope(input, scope); const ULTRA_ROW = thinkingOptIn ? `Provider ultra · ${draft.thinkingLevelMap?.max === "ultra" ? "已启用(Pi max -> ultra)" : "Pi max -> provider ultra(有损 opt-in)"}` : undefined; const BUILTIN_ROW = shouldShowBuiltInCompatRow(scope, draft, inherited) ? `内置compat · ${useBuiltInCompatStateText(draft, inherited)}` : undefined; const CLEAR_SCOPE_ROW = `清除本层覆写(${scopeLabel(scope)})`; const CLEAR_ALL_ROW = "清除该 Provider 全部覆写"; const SAVE_ROW = dirty() ? "保存 ✱" : "保存(无改动)"; const CANCEL_ROW = "取消"; const options = [SCOPE_ROW, PRESET_ROW, REASONING_ROW, CONTEXT_ROW, MAXTOKENS_ROW, THINKING_ROW]; if (ULTRA_ROW) options.push(ULTRA_ROW); if (BUILTIN_ROW) options.push(BUILTIN_ROW); if (cleanModelMeta(draft) || stored) options.push(CLEAR_SCOPE_ROW); if (hasAnyOverride()) options.push(CLEAR_ALL_ROW); options.push(SAVE_ROW, CANCEL_ROW); const pick = await ui.select(title, options); // esc / dismiss if (!pick) { if (await confirmDiscard()) return { kind: "cancel" }; continue; } if (pick === SCOPE_ROW) { await pickScope(); continue; } if (pick === PRESET_ROW) { await pickPreset(); continue; } if (pick === REASONING_ROW) { await pickReasoning(inherited); continue; } if (pick === CONTEXT_ROW) { await pickCount("contextWindow", inherited); continue; } if (pick === MAXTOKENS_ROW) { await pickCount("maxTokens", inherited); continue; } if (pick === THINKING_ROW) { await pickThinkingFormat(inherited, builtIn); continue; } if (ULTRA_ROW && thinkingOptIn && pick === ULTRA_ROW) { const toggled = toggleExactModelThinkingOptIn( draft, scope, scope.kind === "model" ? input.thinkingProjections?.[scope.modelId] : undefined, thinkingOptIn, ); if (!toggled.ok) { ui.notify?.(toggled.error, "warning"); continue; } draft = toggled.modelMeta; requestedThinkingOptIn = toggled.requested; continue; } if (BUILTIN_ROW && pick === BUILTIN_ROW) { await pickUseBuiltInCompat(inherited); continue; } if (pick === CLEAR_SCOPE_ROW) { const ok = await ui.confirm( `清除 ${provider.displayName} · ${scopeLabel(scope)} 的 modelMeta 覆写?`, ); if (ok) return { kind: "clear", scope }; continue; } if (pick === CLEAR_ALL_ROW) { const ok = await ui.confirm( `清除 ${provider.displayName} 的 provider 级与全部按模型覆写?`, ); if (ok) return { kind: "clearAll" }; continue; } if (pick === SAVE_ROW) { if (!dirty()) { ui.notify?.("无改动", "info"); return { kind: "cancel" }; } const cleaned = cleanModelMeta(draft); if (!cleaned) return { kind: "clear", scope }; const activeThinkingOptIn = activeExactModelThinkingOptIn( cleaned, requestedThinkingOptIn, ); return { kind: "save", scope, modelMeta: cleaned, ...(activeThinkingOptIn ? { thinkingOptIn: activeThinkingOptIn } : {}), }; } if (pick === CANCEL_ROW) { if (await confirmDiscard()) return { kind: "cancel" }; continue; } } }