feat: migrate to DSH 0.1.5-rc.2 with TypeScript refactor, official slot footer and codex selector

This commit is contained in:
2026-09-11 13:58:29 +08:00
parent 4401e9cea7
commit fa098b5192
27 changed files with 4976 additions and 6727 deletions
+264
View File
@@ -0,0 +1,264 @@
import React, { useState, useEffect, useMemo, useRef, useSyncExternalStore } from 'react';
import { CODEX_LEVELS_ZH, CODEX_LEVELS_EN } from './styles';
import { isZhLocale } from './i18n';
export interface CodexModelSelectProps {
directory: {
subscribe: (listener: () => void) => () => void;
getSnapshot: () => any;
};
load: () => void;
select: (selection: { provider: string; model: string; reasoningEffort?: string }) => Promise<boolean>;
available: boolean;
locked?: boolean;
}
export function CodexModelSelect(props: CodexModelSelectProps) {
const { directory, load, select, available, locked } = props;
const state = useSyncExternalStore(directory.subscribe, directory.getSnapshot);
const [open, setOpen] = useState(false);
const [showModelPicker, setShowModelPicker] = useState(false);
const rootRef = useRef<HTMLDivElement>(null);
const isZh = isZhLocale();
const codexLevels = isZh ? CODEX_LEVELS_ZH : CODEX_LEVELS_EN;
useEffect(() => {
if (available) load();
}, [available]);
useEffect(() => {
if (!open) return;
const onMouseDown = (e: MouseEvent) => {
if (!rootRef.current?.contains(e.target as Node)) {
setOpen(false);
setShowModelPicker(false);
}
};
document.addEventListener("mousedown", onMouseDown);
return () => document.removeEventListener("mousedown", onMouseDown);
}, [open]);
const choices = useMemo(() => (state.groups || []).flatMap((g: any) => (g.models || []).map((m: any) => ({
group: g,
model: m
}))), [state.groups]);
const currentChoice = choices.find((c: any) => c.group.id === state.current?.provider && c.model.id === state.current?.model);
const currentModel = currentChoice?.model;
const reasoning = currentModel?.reasoning;
const availableEfforts = useMemo(() => {
if (!reasoning || !reasoning.efforts || reasoning.efforts.length === 0) return [];
return reasoning.efforts.map((e: any) => {
const found = codexLevels.find((l) => l.id === e.id);
return {
id: e.id,
label: found?.label || e.name || e.id,
short: found?.short || e.name || e.id
};
});
}, [reasoning, isZh]);
const effectiveEffort = state.current?.reasoningEffort ?? reasoning?.defaultEffort;
const currentEffortIndex = useMemo(() => {
if (availableEfforts.length === 0) return 0;
const idx = availableEfforts.findIndex((e: any) => e.id === effectiveEffort);
return idx === -1 ? 0 : idx;
}, [availableEfforts, effectiveEffort]);
const currentEffortObj = availableEfforts[currentEffortIndex];
const effortDisplayLabel = currentEffortObj ? currentEffortObj.label : (isZh ? "中" : "Medium");
const triggerModelName = currentModel?.name || (isZh ? "选择模型" : "Select model");
const triggerEffortLabel = reasoning ? (currentEffortObj ? currentEffortObj.short : (isZh ? "默认" : "Default")) : null;
const onSelectEffortByIndex = (idx: number) => {
if (!currentChoice || !availableEfforts[idx]) return;
const effort = availableEfforts[idx].id;
select({
provider: currentChoice.group.id,
model: currentChoice.model.id,
reasoningEffort: effort
});
};
const onResetDefault = () => {
if (!currentChoice) return;
select({
provider: currentChoice.group.id,
model: currentChoice.model.id,
reasoningEffort: reasoning?.defaultEffort
});
};
const onTrackClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (availableEfforts.length <= 1) return;
const rect = e.currentTarget.getBoundingClientRect();
const ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
const idx = Math.round(ratio * (availableEfforts.length - 1));
onSelectEffortByIndex(idx);
};
if (!available) return null;
return React.createElement("div", {
ref: rootRef,
style: { position: "relative", display: "inline-flex", alignItems: "center" }
},
React.createElement("button", {
type: "button",
className: `me-codex-trigger ${open ? "active-open" : ""}`,
disabled: locked,
onClick: () => setOpen(!open)
},
React.createElement("span", null, triggerModelName),
triggerEffortLabel ? React.createElement("span", {
style: { color: "#3b82f6", fontWeight: 600 }
}, "· ", triggerEffortLabel) : null,
React.createElement("svg", {
className: `me-codex-trigger-chevron ${open ? "open" : ""}`,
width: "12",
height: "12",
viewBox: "0 0 16 16",
fill: "none",
stroke: "currentColor",
strokeWidth: "1.8"
}, React.createElement("path", { d: "M4 6l4 4 4-4" }))
),
open ? React.createElement("div", {
className: "me-codex-popup"
},
React.createElement("div", {
className: "me-codex-topbar"
},
React.createElement("div", {
className: "me-codex-icon-btn",
title: isZh ? "推理强度" : "Reasoning Effort"
},
React.createElement("svg", {
width: "16",
height: "16",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: "2"
}, React.createElement("path", { d: "M13 2L3 14h9l-1 8 10-12h-9l1-8z" }))
),
React.createElement("div", {
className: "me-codex-center",
onClick: () => setShowModelPicker(!showModelPicker),
title: isZh ? "点击切换模型" : "Click to switch model"
},
React.createElement("div", {
className: "me-codex-effort-title"
},
effortDisplayLabel,
React.createElement("svg", {
width: "12",
height: "12",
viewBox: "0 0 16 16",
fill: "none",
stroke: "currentColor",
strokeWidth: "2",
style: { transform: showModelPicker ? "rotate(90deg)" : undefined, transition: "transform 140ms ease" }
}, React.createElement("path", { d: "M6 3.5L10.5 8L6 12.5" }))
),
React.createElement("div", {
className: "me-codex-model-name"
}, currentModel?.name || (isZh ? "选择模型" : "Select model"))
),
React.createElement("button", {
type: "button",
className: "me-codex-icon-btn",
title: isZh ? "恢复默认档位" : "Reset to default effort",
onClick: onResetDefault
},
React.createElement("svg", {
width: "16",
height: "16",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: "2"
}, React.createElement("path", { d: "M3 12a9 9 0 109-9 9.75 9.75 0 00-6.74 2.74L3 8m0 0V3m0 5h5" }))
)
),
showModelPicker ? React.createElement("div", {
className: "me-model-picker-list"
},
(state.groups || []).map((group: any) => React.createElement("div", {
key: group.id,
className: "me-model-group-section"
},
React.createElement("div", {
className: "me-model-picker-group"
},
React.createElement("svg", {
width: "12",
height: "12",
viewBox: "0 0 16 16",
fill: "none",
stroke: "currentColor",
strokeWidth: "1.8"
}, React.createElement("path", { d: "M2 4h12M2 8h12M2 12h8" })),
group.name
),
(group.models || []).map((m: any) => {
const active = state.current?.provider === group.id && state.current?.model === m.id;
return React.createElement("button", {
key: m.id,
type: "button",
className: `me-model-picker-item ${active ? "active" : ""}`,
onClick: () => {
select({
provider: group.id,
model: m.id,
reasoningEffort: m.reasoning?.defaultEffort
});
setShowModelPicker(false);
}
},
React.createElement("span", null, m.name),
active ? React.createElement("svg", {
width: "14",
height: "14",
viewBox: "0 0 16 16",
fill: "none",
stroke: "#38bdf8",
strokeWidth: "2"
}, React.createElement("path", { d: "M3.5 8.5l3 3 6-6" })) : null
);
})
))
) : React.createElement("div", {
className: "me-slider-track-wrap",
onClick: onTrackClick
},
React.createElement("div", {
className: "me-slider-track"
},
React.createElement("div", {
className: "me-slider-active-fill",
style: {
width: availableEfforts.length <= 1 ? "100%" : `calc(16px + (100% - 32px) * ${currentEffortIndex / (availableEfforts.length - 1)})`
}
}),
React.createElement("div", {
className: "me-slider-dots-layer"
},
availableEfforts.map((eff: any, i: number) => React.createElement("div", {
key: eff.id,
className: `me-slider-dot ${i <= currentEffortIndex ? "lit" : ""}`
}))
),
React.createElement("div", {
className: "me-slider-thumb",
style: {
left: availableEfforts.length <= 1 ? "calc(100% - 16px)" : `calc(16px + (100% - 32px) * ${currentEffortIndex / (availableEfforts.length - 1)})`
}
})
)
)
) : null
);
}
+224
View File
@@ -0,0 +1,224 @@
import React, { useState, useEffect, useMemo } from 'react';
import { fetchAllModelsDev, findDevModel, THINKING_LEVELS_ALL, DevModelInfo } from './modelsDev';
import { isZhLocale, getTranslation } from './i18n';
export interface ModelsFooterSettingsProps {
ctx: any;
}
export function ModelsFooterSettings({ ctx }: ModelsFooterSettingsProps) {
const isZh = isZhLocale();
const [loading, setLoading] = useState(false);
const [providerList, setProviderList] = useState<any[]>([]);
const [selectedProvider, setSelectedProvider] = useState<string>('');
const [statusMsg, setStatusMsg] = useState<{ type: 'ok' | 'err'; text: string } | null>(null);
const [batching, setBatching] = useState(false);
const loadProviders = async () => {
try {
setLoading(true);
const res = await ctx.remote?.llm?.listConfigurableProviders?.();
if (res?.ok && Array.isArray(res.value)) {
setProviderList(res.value);
if (res.value.length > 0 && !selectedProvider) {
setSelectedProvider(res.value[0].provider);
}
}
} catch (e: any) {
console.error('[model-enhancer] Failed to list providers:', e);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadProviders();
}, []);
const handleBatchDevMatch = async () => {
if (!selectedProvider) return;
setBatching(true);
setStatusMsg(null);
try {
const descRes = await ctx.remote?.settings?.describe?.();
const namespaces = descRes?.ok ? descRes.value.namespaces : [];
const piAiNs = namespaces.find((n: any) => n.ns === 'llm-pi-ai');
if (!piAiNs) {
setStatusMsg({ type: 'err', text: 'llm-pi-ai settings namespace not found.' });
return;
}
const provConfig = piAiNs.value?.providers?.[selectedProvider] || {};
const models = Array.isArray(provConfig.models) ? provConfig.models : [];
if (models.length === 0) {
setStatusMsg({ type: 'err', text: getTranslation('fillNoIdError', isZh) });
return;
}
const allDevModels = await fetchAllModelsDev();
let count = 0;
const nextModels = models.map((m: any) => {
if (!m || !m.id) return m;
const matched = findDevModel(m.id, allDevModels);
if (!matched) return m;
count++;
const hasImage = Boolean(matched.modalities?.input?.includes('image'));
const hasReasoning = Boolean(matched.reasoning);
let reasoningLevels: any = undefined;
if (hasReasoning) {
reasoningLevels = { off: "none" };
let added = false;
if (Array.isArray(matched.reasoning_options)) {
for (const opt of matched.reasoning_options) {
if (opt.type === "effort" && Array.isArray(opt.values)) {
for (const val of opt.values) {
if (val === "none" || val === "off") reasoningLevels.off = "none";
else if (THINKING_LEVELS_ALL.includes(val)) {
reasoningLevels[val] = val;
added = true;
}
}
}
}
}
if (!added) {
reasoningLevels.low = "low";
reasoningLevels.medium = "medium";
reasoningLevels.high = "high";
}
}
return {
...m,
name: m.name || matched.name,
contextWindow: m.contextWindow || matched.limit?.context,
maxTokens: m.maxTokens || matched.limit?.output,
input: m.input || (hasImage ? ["text", "image"] : ["text"]),
reasoningEfforts: m.reasoningEfforts !== undefined ? m.reasoningEfforts : (hasReasoning ? reasoningLevels : false)
};
});
// revision-safe mutate
const ops = [{
op: 'set',
path: ['providers', selectedProvider, 'models'],
value: nextModels
}];
const mutateRes = await ctx.remote.settings.mutate('llm-pi-ai', ops, piAiNs.revision);
if (mutateRes?.ok) {
setStatusMsg({
type: 'ok',
text: getTranslation('fillSuccessNotice', isZh, { count, total: models.length })
});
} else {
setStatusMsg({
type: 'err',
text: mutateRes?.error?.message || 'Failed to mutate settings'
});
}
} catch (err: any) {
setStatusMsg({
type: 'err',
text: err.message || String(err)
});
} finally {
setBatching(false);
}
};
return React.createElement("div", {
className: "me-footer-card"
},
React.createElement("div", {
className: "me-footer-title"
},
React.createElement("svg", {
width: "18",
height: "18",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: "2"
}, React.createElement("path", { d: "M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" })),
React.createElement("span", null, getTranslation('enhancerTitle', isZh))
),
React.createElement("div", {
className: "me-footer-desc"
}, getTranslation('enhancerIntro', isZh)),
React.createElement("div", {
style: { display: 'flex', alignItems: 'center', gap: '12px', flexWrap: 'wrap' }
},
React.createElement("label", {
style: { display: 'inline-flex', alignItems: 'center', gap: '6px', fontSize: '13px', fontWeight: 500 }
},
React.createElement("span", null, getTranslation('targetProvider', isZh)),
React.createElement("select", {
value: selectedProvider,
onChange: (e: any) => setSelectedProvider(e.target.value),
style: {
height: '32px',
borderRadius: '6px',
border: '1px solid var(--dsw-alias-border-l2, #ccc)',
background: 'var(--dsw-alias-bg-module-platform, #fff)',
color: 'var(--dsw-alias-label-primary, #000)',
padding: '0 8px',
fontSize: '13px'
}
},
providerList.map((p) => React.createElement("option", { key: p.provider, value: p.provider }, `${p.displayName || p.provider} (${p.settingsNs || 'generic'})`))
)
),
React.createElement("button", {
type: "button",
disabled: batching || !selectedProvider,
onClick: handleBatchDevMatch,
style: {
height: '32px',
borderRadius: '16px',
border: 'none',
background: '#2563eb',
color: '#fff',
padding: '0 14px',
fontSize: '13px',
fontWeight: 500,
cursor: (batching || !selectedProvider) ? 'not-allowed' : 'pointer',
opacity: (batching || !selectedProvider) ? 0.6 : 1,
display: 'inline-flex',
alignItems: 'center',
gap: '6px'
}
},
batching ? getTranslation('batchBtnBusy', isZh) : getTranslation('batchBtn', isZh)
),
React.createElement("button", {
type: "button",
disabled: loading,
onClick: loadProviders,
style: {
height: '32px',
borderRadius: '16px',
border: '1px solid var(--dsw-alias-border-l2, #ccc)',
background: 'transparent',
color: 'var(--dsw-alias-label-secondary, #666)',
padding: '0 12px',
fontSize: '13px',
cursor: 'pointer'
}
}, getTranslation('refreshSettings', isZh))
),
statusMsg ? React.createElement("div", {
style: {
fontSize: '12px',
padding: '6px 10px',
borderRadius: '6px',
background: statusMsg.type === 'ok' ? 'rgba(16, 185, 129, 0.1)' : 'rgba(239, 68, 68, 0.1)',
color: statusMsg.type === 'ok' ? '#10b981' : '#ef4444'
}
}, statusMsg.text) : null
);
}
+162
View File
@@ -0,0 +1,162 @@
import React, { useState } from 'react';
import { fetchAllModelsDev, findDevModel, THINKING_LEVELS_ALL, DevModelInfo } from './modelsDev';
import { isZhLocale, getTranslation } from './i18n';
export interface ProviderCardExtrasProps {
provider: {
provider: string;
displayName: string;
settingsNs: string;
settingsPath: readonly string[];
active: boolean;
declared?: boolean;
error?: string;
};
configured: boolean;
keyConfigured: boolean;
ctx: any;
}
export function ProviderCardExtras({ provider, configured, keyConfigured, ctx }: ProviderCardExtrasProps) {
const isZh = isZhLocale();
const [matching, setMatching] = useState(false);
const [notice, setNotice] = useState<{ type: 'ok' | 'err'; text: string } | null>(null);
if (provider.settingsNs !== 'llm-pi-ai') {
return null;
}
const handleAutoFillProvider = async () => {
setMatching(true);
setNotice(null);
try {
const descRes = await ctx.remote?.settings?.describe?.();
const namespaces = descRes?.ok ? descRes.value.namespaces : [];
const piAiNs = namespaces.find((n: any) => n.ns === 'llm-pi-ai');
if (!piAiNs) {
setNotice({ type: 'err', text: 'llm-pi-ai namespace missing.' });
return;
}
const provKey = provider.provider;
const provConfig = piAiNs.value?.providers?.[provKey] || {};
const models = Array.isArray(provConfig.models) ? provConfig.models : [];
if (models.length === 0) {
setNotice({ type: 'err', text: getTranslation('fillNoIdError', isZh) });
return;
}
const allDevModels = await fetchAllModelsDev();
let count = 0;
const nextModels = models.map((m: any) => {
if (!m || !m.id) return m;
const matched = findDevModel(m.id, allDevModels);
if (!matched) return m;
count++;
const hasImage = Boolean(matched.modalities?.input?.includes('image'));
const hasReasoning = Boolean(matched.reasoning);
let reasoningLevels: any = undefined;
if (hasReasoning) {
reasoningLevels = { off: "none" };
let added = false;
if (Array.isArray(matched.reasoning_options)) {
for (const opt of matched.reasoning_options) {
if (opt.type === "effort" && Array.isArray(opt.values)) {
for (const val of opt.values) {
if (val === "none" || val === "off") reasoningLevels.off = "none";
else if (THINKING_LEVELS_ALL.includes(val)) {
reasoningLevels[val] = val;
added = true;
}
}
}
}
}
if (!added) {
reasoningLevels.low = "low";
reasoningLevels.medium = "medium";
reasoningLevels.high = "high";
}
}
return {
...m,
name: m.name || matched.name,
contextWindow: m.contextWindow || matched.limit?.context,
maxTokens: m.maxTokens || matched.limit?.output,
input: m.input || (hasImage ? ["text", "image"] : ["text"]),
reasoningEfforts: m.reasoningEfforts !== undefined ? m.reasoningEfforts : (hasReasoning ? reasoningLevels : false)
};
});
const ops = [{
op: 'set',
path: ['providers', provKey, 'models'],
value: nextModels
}];
const mutateRes = await ctx.remote.settings.mutate('llm-pi-ai', ops, piAiNs.revision);
if (mutateRes?.ok) {
setNotice({
type: 'ok',
text: getTranslation('fillSuccessNotice', isZh, { count, total: models.length })
});
} else {
setNotice({
type: 'err',
text: mutateRes?.error?.message || 'Mutation rejected'
});
}
} catch (e: any) {
setNotice({ type: 'err', text: e.message || String(e) });
} finally {
setMatching(false);
}
};
return React.createElement("div", {
style: {
padding: '8px 12px',
marginTop: '4px',
background: 'var(--dsw-alias-bg-module-platform, rgba(0, 0, 0, 0.02))',
borderRadius: '8px',
border: '1px dashed var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.1))',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: '8px'
}
},
React.createElement("span", {
style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary, #666)' }
}, getTranslation('activeEnhancerNotice', isZh, { provider: provider.displayName })),
React.createElement("button", {
type: "button",
disabled: matching,
onClick: handleAutoFillProvider,
style: {
height: '26px',
padding: '0 10px',
borderRadius: '13px',
border: 'none',
background: '#2563eb',
color: '#fff',
fontSize: '12px',
fontWeight: 500,
cursor: matching ? 'not-allowed' : 'pointer',
opacity: matching ? 0.7 : 1
}
}, matching ? getTranslation('singleBtnBusy', isZh) : getTranslation('batchBtn', isZh)),
notice ? React.createElement("span", {
style: {
fontSize: '11px',
color: notice.type === 'ok' ? '#10b981' : '#ef4444',
marginLeft: '8px'
}
}, notice.text) : null
);
}
+159
View File
@@ -0,0 +1,159 @@
export interface TranslationMap {
[key: string]: string;
}
export const I18N_DICT: { zh: TranslationMap; en: TranslationMap } = {
zh: {
// ModelListEditor & Badges
batchBtn: "一键补全",
batchBtnBusy: "补全中...",
batchBtnTitle: "根据模型 ID 从 models.dev 自动匹配并预填充缺失参数",
singleBtn: "智能补全",
singleBtnBusy: "补全中",
singleBtnTitle: "从 models.dev 获取该模型推荐参数",
badgeVision: "图像",
badgeVisionTitle: "支持图像输入 (Vision)",
badgeThinking: "思考",
badgeThinkingTitle: "思考档位: {levels}",
defaultEffort: "默认",
modalitiesTitle: "输入模态支持 (Input Modalities)",
modalityText: "文本输入 (Text)",
modalityVision: "图像输入 (Vision)",
reasoningTitle: "推理思考档位 (Reasoning Efforts)",
enableReasoning: "启用深度思考",
saveBlockedHint: "保存受阻提示:模型 {index} {reason}",
fillNoIdError: "列表中没有填写模型 ID 的有效模型",
fillSuccessNotice: "一键补全完成:已匹配并更新 {count}/{total} 个模型",
fillFailedNotice: "一键补全失败: {message}",
singleFillRequireId: "请先填写模型 ID",
singleFillNotFound: '未在 models.dev 中找到与 "{id}" 匹配的模型信息',
singleFillFetchFailed: "获取 models.dev 失败: {message}",
singleFillApplied: '已预填充 "{id}" 推荐参数',
// PrefillModal & Footer Settings
enhancerTitle: "Model Enhancer (多模态与推理参数增强)",
enhancerIntro: "配置 models.dev 预填充、输入模态与推理思考档位,并可选择目标 Provider 一键应用增强配置。",
targetProvider: "目标 Provider",
refreshSettings: "刷新配置",
activeEnhancerNotice: "当前正在对「{provider}」生效增强功能。",
modalTitle: "models.dev 预填充确认与选择",
matchedModelLabel: "检索到匹配模型:",
thSelect: "选用",
thField: "配置项",
thCurrent: "当前设置",
thRecommended: "models.dev 推荐值",
fieldName: "显示名称",
fieldContext: "上下文长度",
fieldMaxTokens: "最大输出",
fieldInput: "图像输入",
fieldReasoning: "推理思考等级",
fieldUnset: "(未设置)",
fieldDefault: "(默认)",
visionSupported: "支持图像",
visionOnlyText: "仅文本",
thinkingOff: "关闭思考",
thinkingOn: "开启",
cancel: "取消",
applySelected: "应用已选推荐项",
// Codex Slider
effortOff: "关",
effortMinimal: "极低",
effortLow: "低",
effortMedium: "中",
effortHigh: "高",
effortXHigh: "超高",
effortMax: "最大",
effortReasoningTitle: "推理强度",
effortSelectModelTip: "点击切换模型",
effortResetTip: "恢复默认档位",
effortChooseModel: "选择模型",
commandsTitle: "命令菜单 (/)"
},
en: {
// ModelListEditor & Badges
batchBtn: "Auto-Fill All",
batchBtnBusy: "Filling…",
batchBtnTitle: "Auto-match and prefill missing parameters from models.dev",
singleBtn: "Auto-Fill",
singleBtnBusy: "Filling…",
singleBtnTitle: "Fetch recommended parameters for this model from models.dev",
badgeVision: "Vision",
badgeVisionTitle: "Supports Image/Vision input",
badgeThinking: "Reasoning",
badgeThinkingTitle: "Reasoning efforts: {levels}",
defaultEffort: "Default",
modalitiesTitle: "Input Modalities",
modalityText: "Text Input",
modalityVision: "Vision Input",
reasoningTitle: "Reasoning Efforts",
enableReasoning: "Enable Deep Reasoning",
saveBlockedHint: "Save blocked: Model {index} {reason}",
fillNoIdError: "No valid models with a Model ID found in the list",
fillSuccessNotice: "Auto-fill complete: matched & updated {count}/{total} models",
fillFailedNotice: "Auto-fill failed: {message}",
singleFillRequireId: "Please enter a Model ID first",
singleFillNotFound: 'No matching model found on models.dev for "{id}"',
singleFillFetchFailed: "Failed to fetch models.dev: {message}",
singleFillApplied: 'Prefilled recommended parameters for "{id}"',
// PrefillModal & Footer Settings
enhancerTitle: "Model Enhancer Settings",
enhancerIntro: "Configure models.dev auto-fill, input modalities and reasoning effort levels across providers.",
targetProvider: "Target Provider:",
refreshSettings: "Refresh",
activeEnhancerNotice: "Enhancer active on \"{provider}\".",
modalTitle: "models.dev Prefill Confirmation",
matchedModelLabel: "Matched Model: ",
thSelect: "Apply",
thField: "Property",
thCurrent: "Current Setting",
thRecommended: "models.dev Value",
fieldName: "Display Name",
fieldContext: "Context Window",
fieldMaxTokens: "Max Output Tokens",
fieldInput: "Vision Input",
fieldReasoning: "Reasoning Efforts",
fieldUnset: "(Unset)",
fieldDefault: "(Default)",
visionSupported: "Vision Supported",
visionOnlyText: "Text Only",
thinkingOff: "Disabled",
thinkingOn: "Enabled",
cancel: "Cancel",
applySelected: "Apply Selected",
// Codex Slider
effortOff: "Off",
effortMinimal: "Minimal",
effortLow: "Low",
effortMedium: "Medium",
effortHigh: "High",
effortXHigh: "X-High",
effortMax: "Max",
effortReasoningTitle: "Reasoning Effort",
effortSelectModelTip: "Click to switch model",
effortResetTip: "Reset to default effort",
effortChooseModel: "Select model",
commandsTitle: "Commands (/)"
}
};
export function isZhLocale(): boolean {
if (typeof navigator !== 'undefined') {
const lang = (navigator.language || '').toLowerCase();
if (lang.startsWith('zh')) return true;
}
return false;
}
export function getTranslation(key: string, isZh = isZhLocale(), params?: Record<string, any>): string {
const dict = isZh ? I18N_DICT.zh : I18N_DICT.en;
let str = dict[key] || I18N_DICT.en[key] || key;
if (params) {
for (const [k, v] of Object.entries(params)) {
str = str.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v));
}
}
return str;
}
+76
View File
@@ -0,0 +1,76 @@
import React from 'react';
import { ENHANCER_CSS } from './styles';
import { CodexModelSelect } from './CodexModelSelect';
import { ModelsFooterSettings } from './ModelsFooterSettings';
import { ProviderCardExtras } from './ProviderCardExtras';
export const name = 'dsh-plugin-model-enhancer';
export const inject = [
'slots',
'remote',
'remote.settings',
'remote.llm',
'modelDirectories',
'sessions'
];
export function apply(ctx: any) {
// Inject CSS once
if (typeof document !== 'undefined' && !document.querySelector("style[data-plugin='dsh-model-enhancer']")) {
const tag = document.createElement("style");
tag.dataset.plugin = "dsh-model-enhancer";
tag.textContent = ENHANCER_CSS;
document.head.appendChild(tag);
}
// 1. Register Codex-style Model and Reasoning Selector in conversation.input.right
ctx.inject(['slots'], (scope: any) => {
const sessions = ctx.get('sessions');
scope.slots.inject('conversation.input.right', () => scope.slots.register({
name: 'conversation.input.right',
id: 'me-codex-model-selector',
order: 100,
inject: (sessionId: string) => {
const dirSvc = ctx.get('modelDirectories');
const directory = dirSvc ? dirSvc.directoryFor(sessionId) : null;
const available = sessions ? sessions.subagentAddress(sessionId) === undefined : true;
return {
available: available && Boolean(directory),
directory: directory ? directory.store : {
subscribe: () => () => {},
getSnapshot: () => ({ status: 'idle', groups: [], current: null })
},
load: () => {
if (available && directory) directory.load().catch(() => {});
},
select: (selection: any) => (available && directory)
? directory.select(selection).then(() => true, () => false)
: Promise.resolve(false)
};
}
}, CodexModelSelect));
});
// 2. Register into official settings extension slot: settings.models.footer
ctx.inject(['slots'], (scope: any) => {
scope.slots.inject('settings.models.footer', () => scope.slots.register({
name: 'settings.models.footer',
id: 'me-models-footer',
order: 50,
inject: () => ({ ctx })
}, ModelsFooterSettings));
});
// 3. Register into official settings extension slot: settings.models.provider-card for llm-pi-ai
ctx.inject(['slots'], (scope: any) => {
scope.slots.inject('settings.models.provider-card', () => scope.slots.register({
name: 'settings.models.provider-card',
key: 'llm-pi-ai',
inject: (ownerProps: any) => ({
...ownerProps,
ctx
})
}, ProviderCardExtras));
});
}
+98
View File
@@ -0,0 +1,98 @@
export interface DevModelInfo {
providerKey: string;
providerName: string;
id: string;
rawKey: string;
name?: string;
limit?: {
context?: number;
output?: number;
};
modalities?: {
input?: string[];
output?: string[];
};
reasoning?: boolean;
reasoning_options?: Array<{
type?: string;
values?: string[];
}>;
[key: string]: any;
}
export const THINKING_LEVELS_ALL = ["minimal", "low", "medium", "high", "xhigh", "max"];
let modelsDevGlobalCache: DevModelInfo[] | null = null;
let modelsDevGlobalPromise: Promise<DevModelInfo[]> | null = null;
export async function fetchAllModelsDev(): Promise<DevModelInfo[]> {
if (modelsDevGlobalCache) return modelsDevGlobalCache;
if (modelsDevGlobalPromise) return modelsDevGlobalPromise;
modelsDevGlobalPromise = (async () => {
const endpoints = ["/api/models-dev/models", "https://models.dev/api.json"];
let lastError: any = null;
for (const url of endpoints) {
try {
const res = await fetch(url);
if (res.ok) {
const json = await res.json();
const list: DevModelInfo[] = [];
for (const [pId, p] of Object.entries(json as Record<string, any>)) {
if (p && p.models) {
for (const [mId, m] of Object.entries(p.models as Record<string, any>)) {
list.push({
providerKey: pId,
providerName: p.name || pId,
id: m.id || mId,
rawKey: mId,
...m
});
}
}
}
modelsDevGlobalCache = list;
return list;
}
} catch (e) {
lastError = e;
}
}
throw lastError || new Error("Failed to load models.dev");
})();
try {
return await modelsDevGlobalPromise;
} finally {
modelsDevGlobalPromise = null;
}
}
export function normModelKey(key: string): string {
if (!key || typeof key !== "string") return "";
return key.toLowerCase()
.replace(/^[a-z0-9_-]+\//, "")
.replace(/[-_.:/]/g, "")
.replace(/\d{8}$/, "");
}
export function findDevModel(queryId: string, allList: DevModelInfo[]): DevModelInfo | null {
if (!queryId || !allList) return null;
const q = queryId.trim().toLowerCase();
const qNorm = normModelKey(q);
let m = allList.find((x) => x.id?.toLowerCase() === q || x.rawKey?.toLowerCase() === q);
if (m) return m;
m = allList.find((x) => x.id?.split("/").pop()?.toLowerCase() === q);
if (m) return m;
m = allList.find((x) => normModelKey(x.id) === qNorm || normModelKey(x.name || "") === qNorm);
if (m) return m;
m = allList.find((x) => {
const n = normModelKey(x.id);
return n.length >= 3 && (n.includes(qNorm) || qNorm.includes(n));
});
return m || null;
}
+340
View File
@@ -0,0 +1,340 @@
export const CODEX_LEVELS_ZH = [
{ id: "off", label: "关", short: "关" },
{ id: "minimal", label: "极低", short: "极低" },
{ id: "low", label: "低", short: "低" },
{ id: "medium", label: "中", short: "中" },
{ id: "high", label: "高", short: "高" },
{ id: "xhigh", label: "超高", short: "超高" },
{ id: "max", label: "最大", short: "最大" }
];
export const CODEX_LEVELS_EN = [
{ id: "off", label: "Off", short: "Off" },
{ id: "minimal", label: "Minimal", short: "Minimal" },
{ id: "low", label: "Low", short: "Low" },
{ id: "medium", label: "Medium", short: "Medium" },
{ id: "high", label: "High", short: "High" },
{ id: "xhigh", label: "X-High", short: "X-High" },
{ id: "max", label: "Max", short: "Max" }
];
export const ENHANCER_CSS = `
/* Hide default trigger when Codex trigger is active */
div[class*="_7KE1Ra_root"], div[class*="ModelSelect_module_css_root"] {
display: none !important;
}
.me-codex-trigger {
box-sizing: border-box;
height: 28px;
padding: 0 8px;
border-radius: 14px;
border: 1px solid transparent;
background: transparent;
color: var(--dsw-alias-label-secondary, #61666b);
font-size: 13px;
font-weight: 500;
display: inline-flex;
align-items: center;
gap: 4px;
cursor: pointer;
outline: none;
transition: all 120ms ease;
user-select: none;
}
.me-codex-trigger:hover:not(:disabled),
.me-codex-trigger.active-open {
border-color: var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.15));
color: var(--dsw-alias-label-primary, #0f1115);
}
body[data-ds-dark-theme] .me-codex-trigger:hover:not(:disabled),
body[data-ds-dark-theme] .me-codex-trigger.active-open {
border-color: var(--dsw-alias-border-l3, rgba(255, 255, 255, 0.2));
color: var(--dsw-alias-label-primary, #fff);
}
.me-codex-trigger-chevron {
transition: transform 140ms ease;
opacity: 0.7;
}
.me-codex-trigger-chevron.open {
transform: rotate(180deg);
}
.me-codex-popup {
position: absolute;
bottom: calc(100% + 8px);
right: 0;
width: 280px;
background: var(--dsw-specific-menu, #ffffff);
border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.08));
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.06);
border-radius: 14px;
padding: 10px 12px 12px;
z-index: 1200;
display: flex;
flex-direction: column;
gap: 8px;
}
body[data-ds-dark-theme] .me-codex-popup {
background: var(--dsw-specific-menu, #1b1d22);
border-color: var(--dsw-alias-border-l3, rgba(255, 255, 255, 0.1));
box-shadow: 0 10px 36px rgba(0, 0, 0, 0.5), 0 2px 10px rgba(0, 0, 0, 0.3);
}
.me-codex-topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.me-codex-icon-btn {
box-sizing: border-box;
width: 26px;
height: 26px;
border-radius: 6px;
border: none;
background: transparent;
color: var(--dsw-alias-label-secondary, #64748b);
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
padding: 0;
}
.me-codex-icon-btn:hover {
background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.05));
color: var(--dsw-alias-label-primary, #0f172a);
}
body[data-ds-dark-theme] .me-codex-icon-btn {
color: var(--dsw-alias-label-secondary, #94a3b8);
}
body[data-ds-dark-theme] .me-codex-icon-btn:hover {
background: var(--dsw-alias-surface-l2, rgba(255, 255, 255, 0.08));
color: #fff;
}
.me-codex-center {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
user-select: none;
}
.me-codex-effort-title {
font-size: 13px;
font-weight: 600;
color: var(--dsw-alias-label-primary, #0f172a);
display: flex;
align-items: center;
gap: 3px;
}
body[data-ds-dark-theme] .me-codex-effort-title {
color: #fff;
}
.me-codex-model-name {
font-size: 11px;
color: var(--dsw-alias-label-tertiary, #64748b);
white-space: nowrap;
max-width: 170px;
overflow: hidden;
text-overflow: ellipsis;
}
.me-codex-model-name:hover {
color: #2563eb;
}
body[data-ds-dark-theme] .me-codex-model-name {
color: var(--dsw-alias-label-tertiary, #94a3b8);
}
body[data-ds-dark-theme] .me-codex-model-name:hover {
color: #60a5fa;
}
.me-slider-track-wrap {
box-sizing: border-box;
padding: 4px 0 2px;
cursor: pointer;
user-select: none;
}
.me-slider-track {
position: relative;
height: 8px;
background: var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.08));
border-radius: 4px;
display: flex;
align-items: center;
}
body[data-ds-dark-theme] .me-slider-track {
background: var(--dsw-alias-border-l3, rgba(255, 255, 255, 0.12));
}
.me-slider-active-fill {
position: absolute;
left: 0;
height: 100%;
border-radius: 4px;
background: #3b82f6;
pointer-events: none;
transition: width 120ms ease;
}
.me-slider-dots-layer {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 14px;
pointer-events: none;
}
.me-slider-dot {
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--dsw-alias-border-l3, rgba(0, 0, 0, 0.2));
}
body[data-ds-dark-theme] .me-slider-dot {
background: rgba(255, 255, 255, 0.25);
}
.me-slider-dot.lit {
background: #fff;
}
.me-slider-thumb {
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
width: 16px;
height: 16px;
border-radius: 50%;
background: #ffffff;
border: 2px solid #3b82f6;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
pointer-events: none;
transition: left 120ms ease;
}
body[data-ds-dark-theme] .me-slider-thumb {
background: #1e293b;
border-color: #60a5fa;
}
.me-model-picker-list {
max-height: 210px;
overflow-y: auto;
padding-right: 2px;
}
.me-model-group-section {
margin-top: 8px;
padding-top: 6px;
border-top: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.08));
}
.me-model-group-section:first-child {
margin-top: 0;
padding-top: 0;
border-top: none;
}
.me-model-picker-group {
font-size: 11px;
color: #2563eb;
font-weight: 600;
padding: 3px 6px;
text-transform: uppercase;
letter-spacing: 0.05em;
display: inline-flex;
align-items: center;
gap: 4px;
background: rgba(37, 99, 235, 0.08);
border-radius: 4px;
margin-bottom: 4px;
}
body[data-ds-dark-theme] .me-model-picker-group {
color: #60a5fa;
background: rgba(59, 130, 246, 0.08);
}
.me-model-picker-item {
box-sizing: border-box;
width: 100%;
padding: 6px 8px;
border-radius: 6px;
background: transparent;
border: none;
color: var(--dsw-alias-label-primary, #0f1115);
font-size: 13px;
text-align: left;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
transition: all 120ms ease;
}
.me-model-picker-item:hover {
background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.05));
}
.me-model-picker-item.active {
background: rgba(37, 99, 235, 0.1);
color: #2563eb;
font-weight: 500;
}
body[data-ds-dark-theme] .me-model-picker-item {
color: var(--dsw-alias-label-primary, #e2e8f0);
}
body[data-ds-dark-theme] .me-model-picker-item:hover {
background: var(--dsw-alias-surface-l2, rgba(255, 255, 255, 0.06));
}
body[data-ds-dark-theme] .me-model-picker-item.active {
background: rgba(56, 189, 248, 0.12);
color: #38bdf8;
}
/* Footer Section */
.me-footer-card {
box-sizing: border-box;
margin-top: 16px;
padding: 16px;
border-radius: 12px;
border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.08));
background: var(--dsw-alias-bg-module-platform, rgba(0, 0, 0, 0.02));
display: flex;
flex-direction: column;
gap: 12px;
}
body[data-ds-dark-theme] .me-footer-card {
border-color: var(--dsw-alias-border-l3, rgba(255, 255, 255, 0.1));
background: var(--dsw-alias-surface-l1, rgba(255, 255, 255, 0.02));
}
.me-footer-title {
font-size: 15px;
font-weight: 600;
color: var(--dsw-alias-label-primary, #0f1115);
display: flex;
align-items: center;
gap: 8px;
}
body[data-ds-dark-theme] .me-footer-title {
color: #fff;
}
.me-footer-desc {
font-size: 13px;
color: var(--dsw-alias-label-secondary, #61666b);
line-height: 20px;
}
.me-diff-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.me-diff-table th, .me-diff-table td {
padding: 6px 8px;
border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.08));
}
body[data-ds-dark-theme] .me-diff-table th, body[data-ds-dark-theme] .me-diff-table td {
border-color: var(--dsw-alias-border-l3, rgba(255, 255, 255, 0.1));
}
.me-val-new {
color: var(--dsw-alias-state-success-primary, #10b981);
font-weight: 500;
}
`;
+73
View File
@@ -0,0 +1,73 @@
export const name = 'dsh-plugin-model-enhancer';
export const inject = ['webServer'];
let cachedData: any = null;
let lastFetchedAt = 0;
const CACHE_TTL_MS = 3600 * 1000; // 1 hour
export async function getModelsDevData() {
const now = Date.now();
if (cachedData && now - lastFetchedAt < CACHE_TTL_MS) {
return cachedData;
}
try {
const res = await fetch('https://models.dev/api.json', {
headers: {
'User-Agent': 'DSH-Model-Enhancer/1.0.0'
}
});
if (!res.ok) {
throw new Error(`Failed to fetch models.dev: HTTP ${res.status}`);
}
const json = await res.json();
cachedData = json;
lastFetchedAt = now;
return json;
} catch (err) {
if (cachedData) {
// Fallback to stale cache if network fails
return cachedData;
}
throw err;
}
}
export function apply(ctx: any) {
// 1. Models.dev Cache Proxy
ctx.effect(() => {
return ctx.webServer.register({
kind: 'exact',
path: '/api/models-dev/models',
handler: async (req: any, res: any) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') {
res.writeHead(204);
res.end();
return;
}
if (req.method !== 'GET') {
res.writeHead(405, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Method Not Allowed' }));
return;
}
try {
const data = await getModelsDevData();
res.writeHead(200, {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=3600'
});
res.end(JSON.stringify(data));
} catch (err: any) {
ctx.logger?.error?.('[model-enhancer] Error fetching models.dev:', err);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: err.message || 'Internal Server Error' }));
}
}
});
}, 'dsh-plugin-model-enhancer: webServer proxy route');
}