fix: adapt model enhancements for DSH 0.1.5

This commit is contained in:
2026-09-11 21:45:40 +08:00
parent b9e422ad30
commit 77304500b1
19 changed files with 1583 additions and 1525 deletions
+6 -10
View File
@@ -1,6 +1,6 @@
# dsh-plugin-model-enhancer
# dsh-plugin-model-enhancer (adapted for DSH 0.1.5-rc.2)
DeepSeek Harness (DSH) Model Settings Enhancer & Codex Selector Plugin (Compatible with DSH 0.1.5-rc.2).
DeepSeek Harness (DSH) Model Settings Enhancer Plugin (Adapted for DSH 0.1.5-rc.2).
## ✨ Features
@@ -8,17 +8,13 @@ DeepSeek Harness (DSH) Model Settings Enhancer & Codex Selector Plugin (Compatib
- Integrates model enhancement configuration via `settings.models.footer` and `settings.models.provider-card` (targeted at `llm-pi-ai`).
- Supports models.dev auto-fill matching, prefilling missing capacities, image modalities (`input: ["text", "image"]`), and multi-level reasoning efforts (`minimal` / `low` / `medium` / `high` / `xhigh` / `max`).
- Uses `ctx.remote.settings.mutate` with revision fence checking for safe, non-destructive configuration persistence.
- Preserves official file attachment and upload pipeline (`ui-attachment` & `file-upload`), removing obsolete custom upload endpoint routes.
- Preserves official file attachment and upload pipeline (`ui-attachment` & `file-upload`).
- Delegates session-level model and reasoning selection completely to official DSH 0.1.5-rc.2 core components.
2. **Codex-style Model & Reasoning Effort Selector**:
- Injected into `conversation.input.right` seamlessly alongside the official composer.
- Reuses `ctx.modelDirectories` (`ModelDirectoryResolver`) for per-session directory resolution and selection dispatch.
- Interactive sliding track for reasoning effort adjustments and grouped model picker popup.
3. **models.dev Cache & Proxy**:
2. **models.dev Cache & Proxy**:
- Exposes exact WebServer proxy route `/api/models-dev/models` with in-memory caching and fallback.
4. **Full Bilingual Support (i18n)**:
3. **Full Bilingual Support (i18n)**:
- Dynamic localization supporting Simplified Chinese (`zh`) and English (`en`).
## 🛠 Building & Development
+175 -780
View File
File diff suppressed because it is too large Load Diff
-23
View File
@@ -1,23 +0,0 @@
import React from 'react';
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 declare function CodexModelSelect(props: CodexModelSelectProps): React.DetailedReactHTMLElement<{
ref: React.MutableRefObject<HTMLDivElement>;
style: {
position: "relative";
display: "inline-flex";
alignItems: "center";
};
}, HTMLDivElement>;
+2
View File
@@ -21,5 +21,7 @@ export interface DevModelInfo {
}
export declare const THINKING_LEVELS_ALL: string[];
export declare function fetchAllModelsDev(): Promise<DevModelInfo[]>;
export declare function sanitizePositiveInt(val: unknown): number | undefined;
export declare function mergeModelCapabilities(m: any, matched?: DevModelInfo | null): any;
export declare function normModelKey(key: string): string;
export declare function findDevModel(queryId: string, allList: DevModelInfo[]): DevModelInfo | null;
+1 -11
View File
File diff suppressed because one or more lines are too long
-1
View File
@@ -32,7 +32,6 @@
"@deepseek-ai/dsh-client-ui-settings",
"@deepseek-ai/dsh-client-ui-settings-models",
"@deepseek-ai/dsh-client-ui-slots",
"@deepseek-ai/dsh-client-ui-model-selection",
"@deepseek-ai/dsh-api-remotes"
]
}
+1090
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
allowBuilds:
esbuild: set this to true or false
-264
View File
@@ -1,264 +0,0 @@
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
);
}
+3 -38
View File
@@ -1,5 +1,5 @@
import React, { useState, useEffect, useMemo } from 'react';
import { fetchAllModelsDev, findDevModel, THINKING_LEVELS_ALL, DevModelInfo } from './modelsDev';
import { fetchAllModelsDev, findDevModel, mergeModelCapabilities } from './modelsDev';
import { isZhLocale, getTranslation } from './i18n';
export interface ModelsFooterSettingsProps {
@@ -63,43 +63,8 @@ export function ModelsFooterSettings({ ctx }: ModelsFooterSettingsProps) {
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)
};
if (matched) count++;
return mergeModelCapabilities(m, matched);
});
// revision-safe mutate
+3 -38
View File
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { fetchAllModelsDev, findDevModel, THINKING_LEVELS_ALL, DevModelInfo } from './modelsDev';
import { fetchAllModelsDev, findDevModel, mergeModelCapabilities } from './modelsDev';
import { isZhLocale, getTranslation } from './i18n';
export interface ProviderCardExtrasProps {
@@ -54,43 +54,8 @@ export function ProviderCardExtras({ provider, configured, keyConfigured, ctx }:
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)
};
if (matched) count++;
return mergeModelCapabilities(m, matched);
});
const ops = [{
+2 -30
View File
@@ -54,21 +54,7 @@ export const I18N_DICT: { zh: TranslationMap; en: TranslationMap } = {
thinkingOff: "关闭思考",
thinkingOn: "开启",
cancel: "取消",
applySelected: "应用已选推荐项",
// Codex Slider
effortOff: "关",
effortMinimal: "极低",
effortLow: "低",
effortMedium: "中",
effortHigh: "高",
effortXHigh: "超高",
effortMax: "最大",
effortReasoningTitle: "推理强度",
effortSelectModelTip: "点击切换模型",
effortResetTip: "恢复默认档位",
effortChooseModel: "选择模型",
commandsTitle: "命令菜单 (/)"
applySelected: "应用已选推荐项"
},
en: {
// ModelListEditor & Badges
@@ -121,21 +107,7 @@ export const I18N_DICT: { zh: TranslationMap; en: TranslationMap } = {
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 (/)"
applySelected: "Apply Selected"
}
};
+3 -34
View File
@@ -1,6 +1,5 @@
import React from 'react';
import { ENHANCER_CSS } from './styles';
import { CodexModelSelect } from './CodexModelSelect';
import { ModelsFooterSettings } from './ModelsFooterSettings';
import { ProviderCardExtras } from './ProviderCardExtras';
@@ -10,9 +9,7 @@ export const inject = [
'slots',
'remote',
'remote.settings',
'remote.llm',
'modelDirectories',
'sessions'
'remote.llm'
];
export function apply(ctx: any) {
@@ -24,35 +21,7 @@ export function apply(ctx: any) {
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
// 1. 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',
@@ -62,7 +31,7 @@ export function apply(ctx: any) {
}, ModelsFooterSettings));
});
// 3. Register into official settings extension slot: settings.models.provider-card for llm-pi-ai
// 2. 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',
+84
View File
@@ -68,6 +68,90 @@ export async function fetchAllModelsDev(): Promise<DevModelInfo[]> {
}
}
export function sanitizePositiveInt(val: unknown): number | undefined {
if (typeof val === 'number' && Number.isFinite(val) && Math.floor(val) === val && val >= 1) {
return val;
}
if (typeof val === 'string' && /^\d+$/.test(val.trim())) {
const parsed = parseInt(val.trim(), 10);
if (Number.isFinite(parsed) && parsed >= 1) {
return parsed;
}
}
return undefined;
}
export function mergeModelCapabilities(m: any, matched?: DevModelInfo | null): any {
if (!m) return m;
if (!matched) {
const result: any = { ...m };
const validExistingContext = sanitizePositiveInt(m.contextWindow);
if (validExistingContext !== undefined) {
result.contextWindow = validExistingContext;
} else {
delete result.contextWindow;
}
const validExistingMaxTokens = sanitizePositiveInt(m.maxTokens);
if (validExistingMaxTokens !== undefined) {
result.maxTokens = validExistingMaxTokens;
} else {
delete result.maxTokens;
}
return result;
}
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";
}
}
const result: any = {
...m,
name: m.name || matched.name,
input: m.input || (hasImage ? ["text", "image"] : ["text"]),
reasoningEfforts: m.reasoningEfforts !== undefined ? m.reasoningEfforts : (hasReasoning ? reasoningLevels : false)
};
const resolvedContext = sanitizePositiveInt(m.contextWindow) ?? sanitizePositiveInt(matched.limit?.context);
if (resolvedContext !== undefined) {
result.contextWindow = resolvedContext;
} else {
delete result.contextWindow;
}
const resolvedMaxTokens = sanitizePositiveInt(m.maxTokens) ?? sanitizePositiveInt(matched.limit?.output);
if (resolvedMaxTokens !== undefined) {
result.maxTokens = resolvedMaxTokens;
} else {
delete result.maxTokens;
}
return result;
}
export function normModelKey(key: string): string {
if (!key || typeof key !== "string") return "";
return key.toLowerCase()
+1 -291
View File
@@ -1,295 +1,5 @@
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 */
export const ENHANCER_CSS = `
.me-footer-card {
box-sizing: border-box;
margin-top: 16px;
+62 -2
View File
@@ -1,5 +1,8 @@
import { describe, it, expect, vi } from 'vitest';
import { apply } from '../src/index';
import { apply as applyHost } from '../src/index';
import * as clientModule from '../src/client/index';
import { ENHANCER_CSS } from '../src/client/styles';
import React from 'react';
describe('Host plugin', () => {
it('registers models dev exact proxy route', () => {
@@ -11,10 +14,67 @@ describe('Host plugin', () => {
},
};
apply(mockCtx as any);
applyHost(mockCtx as any);
expect(registerMock).toHaveBeenCalled();
const callArg = registerMock.mock.calls[0][0];
expect(callArg.path).toBe('/api/models-dev/models');
expect(callArg.kind).toBe('exact');
});
});
describe('Client plugin contracts', () => {
it('registers settings extension slots only and leaves model selection untouched', async () => {
const registeredSlots: string[] = [];
const registeredEntries: any[] = [];
const mockScope = {
slots: {
inject: (slotName: string, cb: any) => {
registeredSlots.push(slotName);
cb();
},
register: vi.fn((def: any, component: any) => {
registeredEntries.push({ def, component });
return def;
}),
}
};
const mockCtx = {
inject: vi.fn((deps: string[], cb: any) => {
cb(mockScope);
}),
get: vi.fn(),
};
clientModule.apply(mockCtx as any);
// Verify slots registered
expect(registeredSlots).toContain('settings.models.footer');
expect(registeredSlots).toContain('settings.models.provider-card');
// Ensure no composer / model-selection slots are registered
expect(registeredSlots).not.toContain('conversation.input.right');
expect(registeredSlots).not.toContain('conversation.input.model');
// Verify footer registration definition
const footerEntry = registeredEntries.find(e => e.def.name === 'settings.models.footer');
expect(footerEntry).toBeDefined();
expect(footerEntry.def.id).toBe('me-models-footer');
// Verify provider card registration definition
const cardEntry = registeredEntries.find(e => e.def.name === 'settings.models.provider-card');
expect(cardEntry).toBeDefined();
expect(cardEntry.def.key).toBe('llm-pi-ai');
});
it('ENHANCER_CSS contains only non-selector settings styles', () => {
expect(ENHANCER_CSS).toContain('.me-footer-card');
expect(ENHANCER_CSS).toContain('.me-footer-title');
expect(ENHANCER_CSS).toContain('.me-diff-table');
// Ensure no selector styles or hide rules remain
expect(ENHANCER_CSS).not.toContain('me-compact');
expect(ENHANCER_CSS).not.toContain('me-slider');
expect(ENHANCER_CSS).not.toContain('me-effort');
expect(ENHANCER_CSS).not.toContain('display: none');
});
});
+21
View File
@@ -0,0 +1,21 @@
export default {
createElement: () => ({}),
Fragment: () => ({}),
useState: (initial: any) => [initial, () => {}],
useEffect: () => {},
useLayoutEffect: () => {},
useMemo: (fn: any) => fn(),
useRef: (val: any) => ({ current: val }),
useSyncExternalStore: () => ({})
};
export const createElement = () => ({});
export const jsx = () => ({});
export const jsxs = () => ({});
export const jsxDEV = () => ({});
export const Fragment = () => ({});
export const useState = (initial: any) => [initial, () => {}];
export const useEffect = () => {};
export const useLayoutEffect = () => {};
export const useMemo = (fn: any) => fn();
export const useRef = (val: any) => ({ current: val });
export const useSyncExternalStore = () => ({});
+109 -3
View File
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { normModelKey, findDevModel } from '../src/client/modelsDev';
import { normModelKey, findDevModel, sanitizePositiveInt, mergeModelCapabilities } from '../src/client/modelsDev';
import { I18N_DICT } from '../src/client/i18n';
describe('modelsDev utils', () => {
@@ -21,12 +21,118 @@ describe('modelsDev utils', () => {
expect(findDevModel('deepseek-r1', list)?.id).toBe('deepseek/deepseek-r1');
});
it('sanitizes positive integers correctly and filters non-positive or non-finite values', () => {
expect(sanitizePositiveInt(128000)).toBe(128000);
expect(sanitizePositiveInt(1)).toBe(1);
expect(sanitizePositiveInt('4096')).toBe(4096);
expect(sanitizePositiveInt(0)).toBeUndefined();
expect(sanitizePositiveInt(-10)).toBeUndefined();
expect(sanitizePositiveInt(NaN)).toBeUndefined();
expect(sanitizePositiveInt(Infinity)).toBeUndefined();
expect(sanitizePositiveInt(-Infinity)).toBeUndefined();
expect(sanitizePositiveInt(128.5)).toBeUndefined();
expect(sanitizePositiveInt('0')).toBeUndefined();
expect(sanitizePositiveInt('-1')).toBeUndefined();
expect(sanitizePositiveInt('abc')).toBeUndefined();
expect(sanitizePositiveInt(null)).toBeUndefined();
expect(sanitizePositiveInt(undefined)).toBeUndefined();
});
it('merges model capabilities without writing zero or invalid contextWindow/maxTokens', () => {
const matchedWithZero = {
id: 'test/model-zero',
rawKey: 'model-zero',
name: 'Model Zero',
limit: {
context: 0,
output: 0
},
modalities: { input: ['text'] },
reasoning: false
};
const targetModel = {
id: 'model-zero',
name: 'Existing Name'
};
const merged = mergeModelCapabilities(targetModel, matchedWithZero as any);
expect(merged.id).toBe('model-zero');
expect(merged.name).toBe('Existing Name');
expect(merged.contextWindow).toBeUndefined();
expect('contextWindow' in merged).toBe(false);
expect(merged.maxTokens).toBeUndefined();
expect('maxTokens' in merged).toBe(false);
});
it('preserves valid existing contextWindow/maxTokens when incoming is zero or missing', () => {
const matchedWithZero = {
id: 'test/model-zero',
rawKey: 'model-zero',
name: 'Model Zero',
limit: {
context: 0,
output: 0
}
};
const existingModel = {
id: 'model-zero',
contextWindow: 131072,
maxTokens: 8192
};
const merged = mergeModelCapabilities(existingModel, matchedWithZero as any);
expect(merged.contextWindow).toBe(131072);
expect(merged.maxTokens).toBe(8192);
});
it('populates valid positive contextWindow/maxTokens from models.dev when existing is missing', () => {
const matchedValid = {
id: 'openai/gpt-4o',
rawKey: 'gpt-4o',
name: 'GPT-4o',
limit: {
context: 128000,
output: 16384
}
};
const existingModel = {
id: 'gpt-4o'
};
const merged = mergeModelCapabilities(existingModel, matchedValid as any);
expect(merged.contextWindow).toBe(128000);
expect(merged.maxTokens).toBe(16384);
});
it('cleans up invalid existing values if present and matched is missing or zero', () => {
const matchedWithZero = {
id: 'test/model-bad',
rawKey: 'model-bad',
limit: {
context: 0,
output: -1
}
};
const existingModelWithZero = {
id: 'model-bad',
contextWindow: 0,
maxTokens: -5
};
const merged = mergeModelCapabilities(existingModelWithZero, matchedWithZero as any);
expect('contextWindow' in merged).toBe(false);
expect('maxTokens' in merged).toBe(false);
});
it('provides bilingual dictionary keys', () => {
expect(I18N_DICT.zh.modalTitle).toBeDefined();
expect(I18N_DICT.en.modalTitle).toBeDefined();
expect(I18N_DICT.zh.batchBtn).toBeDefined();
expect(I18N_DICT.en.batchBtn).toBeDefined();
expect(I18N_DICT.zh.effortReasoningTitle).toBeDefined();
expect(I18N_DICT.en.effortReasoningTitle).toBeDefined();
});
});
+19
View File
@@ -0,0 +1,19 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
esbuild: {
jsx: 'transform',
jsxFactory: 'React.createElement',
jsxFragment: 'React.Fragment'
},
resolve: {
alias: {
'react/jsx-runtime': new URL('./tests/mocks/react.ts', import.meta.url).pathname,
'react/jsx-dev-runtime': new URL('./tests/mocks/react.ts', import.meta.url).pathname,
react: new URL('./tests/mocks/react.ts', import.meta.url).pathname
}
},
test: {
environment: 'node'
}
});