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
|
||||
|
||||
@@ -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
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 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;
|
||||
|
||||
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-models",
|
||||
"@deepseek-ai/dsh-client-ui-slots",
|
||||
"@deepseek-ai/dsh-client-ui-model-selection",
|
||||
"@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 { 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
|
||||
|
||||
@@ -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
@@ -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
@@ -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',
|
||||
|
||||
@@ -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
@@ -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
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 { 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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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