fix: adapt model enhancements for DSH 0.1.5
This commit is contained in:
@@ -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
|
## ✨ 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`).
|
- 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`).
|
- 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.
|
- 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**:
|
2. **models.dev Cache & Proxy**:
|
||||||
- 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**:
|
|
||||||
- Exposes exact WebServer proxy route `/api/models-dev/models` with in-memory caching and fallback.
|
- 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`).
|
- Dynamic localization supporting Simplified Chinese (`zh`) and English (`en`).
|
||||||
|
|
||||||
## 🛠 Building & Development
|
## 🛠 Building & Development
|
||||||
|
|||||||
+175
-780
File diff suppressed because it is too large
Load Diff
-23
@@ -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>;
|
|
||||||
Vendored
+2
@@ -21,5 +21,7 @@ export interface DevModelInfo {
|
|||||||
}
|
}
|
||||||
export declare const THINKING_LEVELS_ALL: string[];
|
export declare const THINKING_LEVELS_ALL: string[];
|
||||||
export declare function fetchAllModelsDev(): Promise<DevModelInfo[]>;
|
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 normModelKey(key: string): string;
|
||||||
export declare function findDevModel(queryId: string, allList: DevModelInfo[]): DevModelInfo | null;
|
export declare function findDevModel(queryId: string, allList: DevModelInfo[]): DevModelInfo | null;
|
||||||
|
|||||||
Vendored
+1
-11
File diff suppressed because one or more lines are too long
@@ -32,7 +32,6 @@
|
|||||||
"@deepseek-ai/dsh-client-ui-settings",
|
"@deepseek-ai/dsh-client-ui-settings",
|
||||||
"@deepseek-ai/dsh-client-ui-settings-models",
|
"@deepseek-ai/dsh-client-ui-settings-models",
|
||||||
"@deepseek-ai/dsh-client-ui-slots",
|
"@deepseek-ai/dsh-client-ui-slots",
|
||||||
"@deepseek-ai/dsh-client-ui-model-selection",
|
|
||||||
"@deepseek-ai/dsh-api-remotes"
|
"@deepseek-ai/dsh-api-remotes"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+1090
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
|||||||
|
allowBuilds:
|
||||||
|
esbuild: set this to true or false
|
||||||
@@ -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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, useMemo } from 'react';
|
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';
|
import { isZhLocale, getTranslation } from './i18n';
|
||||||
|
|
||||||
export interface ModelsFooterSettingsProps {
|
export interface ModelsFooterSettingsProps {
|
||||||
@@ -63,43 +63,8 @@ export function ModelsFooterSettings({ ctx }: ModelsFooterSettingsProps) {
|
|||||||
const nextModels = models.map((m: any) => {
|
const nextModels = models.map((m: any) => {
|
||||||
if (!m || !m.id) return m;
|
if (!m || !m.id) return m;
|
||||||
const matched = findDevModel(m.id, allDevModels);
|
const matched = findDevModel(m.id, allDevModels);
|
||||||
if (!matched) return m;
|
if (matched) count++;
|
||||||
count++;
|
return mergeModelCapabilities(m, matched);
|
||||||
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
|
// revision-safe mutate
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
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';
|
import { isZhLocale, getTranslation } from './i18n';
|
||||||
|
|
||||||
export interface ProviderCardExtrasProps {
|
export interface ProviderCardExtrasProps {
|
||||||
@@ -54,43 +54,8 @@ export function ProviderCardExtras({ provider, configured, keyConfigured, ctx }:
|
|||||||
const nextModels = models.map((m: any) => {
|
const nextModels = models.map((m: any) => {
|
||||||
if (!m || !m.id) return m;
|
if (!m || !m.id) return m;
|
||||||
const matched = findDevModel(m.id, allDevModels);
|
const matched = findDevModel(m.id, allDevModels);
|
||||||
if (!matched) return m;
|
if (matched) count++;
|
||||||
count++;
|
return mergeModelCapabilities(m, matched);
|
||||||
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 = [{
|
const ops = [{
|
||||||
|
|||||||
+2
-30
@@ -54,21 +54,7 @@ export const I18N_DICT: { zh: TranslationMap; en: TranslationMap } = {
|
|||||||
thinkingOff: "关闭思考",
|
thinkingOff: "关闭思考",
|
||||||
thinkingOn: "开启",
|
thinkingOn: "开启",
|
||||||
cancel: "取消",
|
cancel: "取消",
|
||||||
applySelected: "应用已选推荐项",
|
applySelected: "应用已选推荐项"
|
||||||
|
|
||||||
// Codex Slider
|
|
||||||
effortOff: "关",
|
|
||||||
effortMinimal: "极低",
|
|
||||||
effortLow: "低",
|
|
||||||
effortMedium: "中",
|
|
||||||
effortHigh: "高",
|
|
||||||
effortXHigh: "超高",
|
|
||||||
effortMax: "最大",
|
|
||||||
effortReasoningTitle: "推理强度",
|
|
||||||
effortSelectModelTip: "点击切换模型",
|
|
||||||
effortResetTip: "恢复默认档位",
|
|
||||||
effortChooseModel: "选择模型",
|
|
||||||
commandsTitle: "命令菜单 (/)"
|
|
||||||
},
|
},
|
||||||
en: {
|
en: {
|
||||||
// ModelListEditor & Badges
|
// ModelListEditor & Badges
|
||||||
@@ -121,21 +107,7 @@ export const I18N_DICT: { zh: TranslationMap; en: TranslationMap } = {
|
|||||||
thinkingOff: "Disabled",
|
thinkingOff: "Disabled",
|
||||||
thinkingOn: "Enabled",
|
thinkingOn: "Enabled",
|
||||||
cancel: "Cancel",
|
cancel: "Cancel",
|
||||||
applySelected: "Apply Selected",
|
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 (/)"
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+3
-34
@@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ENHANCER_CSS } from './styles';
|
import { ENHANCER_CSS } from './styles';
|
||||||
import { CodexModelSelect } from './CodexModelSelect';
|
|
||||||
import { ModelsFooterSettings } from './ModelsFooterSettings';
|
import { ModelsFooterSettings } from './ModelsFooterSettings';
|
||||||
import { ProviderCardExtras } from './ProviderCardExtras';
|
import { ProviderCardExtras } from './ProviderCardExtras';
|
||||||
|
|
||||||
@@ -10,9 +9,7 @@ export const inject = [
|
|||||||
'slots',
|
'slots',
|
||||||
'remote',
|
'remote',
|
||||||
'remote.settings',
|
'remote.settings',
|
||||||
'remote.llm',
|
'remote.llm'
|
||||||
'modelDirectories',
|
|
||||||
'sessions'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export function apply(ctx: any) {
|
export function apply(ctx: any) {
|
||||||
@@ -24,35 +21,7 @@ export function apply(ctx: any) {
|
|||||||
document.head.appendChild(tag);
|
document.head.appendChild(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Register Codex-style Model and Reasoning Selector in conversation.input.right
|
// 1. Register into official settings extension slot: settings.models.footer
|
||||||
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) => {
|
ctx.inject(['slots'], (scope: any) => {
|
||||||
scope.slots.inject('settings.models.footer', () => scope.slots.register({
|
scope.slots.inject('settings.models.footer', () => scope.slots.register({
|
||||||
name: 'settings.models.footer',
|
name: 'settings.models.footer',
|
||||||
@@ -62,7 +31,7 @@ export function apply(ctx: any) {
|
|||||||
}, ModelsFooterSettings));
|
}, 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) => {
|
ctx.inject(['slots'], (scope: any) => {
|
||||||
scope.slots.inject('settings.models.provider-card', () => scope.slots.register({
|
scope.slots.inject('settings.models.provider-card', () => scope.slots.register({
|
||||||
name: 'settings.models.provider-card',
|
name: 'settings.models.provider-card',
|
||||||
|
|||||||
@@ -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 {
|
export function normModelKey(key: string): string {
|
||||||
if (!key || typeof key !== "string") return "";
|
if (!key || typeof key !== "string") return "";
|
||||||
return key.toLowerCase()
|
return key.toLowerCase()
|
||||||
|
|||||||
+1
-291
@@ -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 */
|
/* Footer Section */
|
||||||
|
export const ENHANCER_CSS = `
|
||||||
.me-footer-card {
|
.me-footer-card {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
|
|||||||
+62
-2
@@ -1,5 +1,8 @@
|
|||||||
import { describe, it, expect, vi } from 'vitest';
|
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', () => {
|
describe('Host plugin', () => {
|
||||||
it('registers models dev exact proxy route', () => {
|
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();
|
expect(registerMock).toHaveBeenCalled();
|
||||||
const callArg = registerMock.mock.calls[0][0];
|
const callArg = registerMock.mock.calls[0][0];
|
||||||
expect(callArg.path).toBe('/api/models-dev/models');
|
expect(callArg.path).toBe('/api/models-dev/models');
|
||||||
expect(callArg.kind).toBe('exact');
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -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
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
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';
|
import { I18N_DICT } from '../src/client/i18n';
|
||||||
|
|
||||||
describe('modelsDev utils', () => {
|
describe('modelsDev utils', () => {
|
||||||
@@ -21,12 +21,118 @@ describe('modelsDev utils', () => {
|
|||||||
expect(findDevModel('deepseek-r1', list)?.id).toBe('deepseek/deepseek-r1');
|
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', () => {
|
it('provides bilingual dictionary keys', () => {
|
||||||
expect(I18N_DICT.zh.modalTitle).toBeDefined();
|
expect(I18N_DICT.zh.modalTitle).toBeDefined();
|
||||||
expect(I18N_DICT.en.modalTitle).toBeDefined();
|
expect(I18N_DICT.en.modalTitle).toBeDefined();
|
||||||
expect(I18N_DICT.zh.batchBtn).toBeDefined();
|
expect(I18N_DICT.zh.batchBtn).toBeDefined();
|
||||||
expect(I18N_DICT.en.batchBtn).toBeDefined();
|
expect(I18N_DICT.en.batchBtn).toBeDefined();
|
||||||
expect(I18N_DICT.zh.effortReasoningTitle).toBeDefined();
|
|
||||||
expect(I18N_DICT.en.effortReasoningTitle).toBeDefined();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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'
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user