feat(aliases): generate default names and improve Fast toggle
This commit is contained in:
1 parent
6000b3159b
commit
d698510473
7 files changed
+254
-36
No files matched your search
@@ -1,6 +1,10 @@
|
||||
import type { MessageKey } from './resources';
|
||||
|
||||
export const jaOverrides = {
|
||||
'apiAccess.models.add': 'カスタムモデルを追加',
|
||||
'apiAccess.models.namePlaceholder': 'モデル名(例: gpt-4o)',
|
||||
'apiAccess.models.aliasPlaceholder': 'モデル別名(任意)',
|
||||
'apiAccess.models.remove': 'モデルを削除',
|
||||
'app.language': '言語',
|
||||
'app.theme.light': 'ライトモード',
|
||||
'app.theme.dark': 'ダークモード',
|
||||
@@ -206,6 +210,7 @@ export const jaOverrides = {
|
||||
'aliases.error.emptyEffort': '推論レベルを入力してください',
|
||||
'aliases.error.emptyOptions': '推論レベルを設定するか、Fast を有効にしてください',
|
||||
'aliases.error.unsupportedEffort': 'このモデルでは推論レベルを設定できないため、Fast オプションのみ使用できます',
|
||||
'aliases.error.unsupportedFast': 'このモデルでは Fast を利用できません',
|
||||
'aliases.created': '{alias} を作成し、推論レベルを {effort} に固定しました',
|
||||
'aliases.createdCombined': '{alias} を作成し、推論レベル {effort} と Fast を有効にしました',
|
||||
'aliases.deleteConfirm': 'エイリアスモデル「{alias}」を削除しますか?\n対応する推論レベルと Fast の上書きルールも削除されます。',
|
||||
@@ -237,6 +242,7 @@ export const jaOverrides = {
|
||||
'aliases.customHelp': '対象モデルが実際に対応するレベル名のみ入力してください',
|
||||
'aliases.aliasName.title': 'エイリアスモデル名',
|
||||
'aliases.aliasName.description': '新しい入口のみを追加し、元のモデル名は変更しません',
|
||||
'aliases.aliasName.autoDescription': 'モデルと選択したオプションから自動生成します。元のモデル名は変更せず編集できます',
|
||||
'aliases.aliasName.example': '例:{model}-{effort}',
|
||||
'aliases.aliasName.selectFirst': '先に元のモデルを選択し、クライアントで使用するエイリアスを入力してください',
|
||||
'aliases.notSelected': 'モデル未選択',
|
||||
|
||||
@@ -4,6 +4,10 @@ import { jaOverrides } from './ja';
|
||||
export type MessageVariables = Record<string, string | number>;
|
||||
|
||||
export const zhCN = {
|
||||
'apiAccess.models.add': '添加自定义模型',
|
||||
'apiAccess.models.namePlaceholder': '模型名称,例如 gpt-4o',
|
||||
'apiAccess.models.aliasPlaceholder': '模型别名(可选)',
|
||||
'apiAccess.models.remove': '删除模型',
|
||||
'app.language': '语言',
|
||||
'app.theme.light': '日间模式',
|
||||
'app.theme.dark': '夜间模式',
|
||||
@@ -209,6 +213,7 @@ export const zhCN = {
|
||||
'aliases.error.emptyEffort': '思考强度不能为空',
|
||||
'aliases.error.emptyOptions': '请至少设置思考强度或启用 Fast',
|
||||
'aliases.error.unsupportedEffort': '该模型不支持设置思考强度,只能使用 Fast 选项',
|
||||
'aliases.error.unsupportedFast': '该模型当前不支持 Fast 选项',
|
||||
'aliases.created': '已创建 {alias},思考强度固定为 {effort}',
|
||||
'aliases.createdCombined': '已创建 {alias},思考强度为 {effort},同时启用 Fast',
|
||||
'aliases.deleteConfirm': '确定删除别名模型“{alias}”吗?\n将同时移除对应的思考强度和 Fast 覆盖规则。',
|
||||
@@ -240,6 +245,7 @@ export const zhCN = {
|
||||
'aliases.customHelp': '仅填写目标模型实际支持的等级名称',
|
||||
'aliases.aliasName.title': '别名模型名称',
|
||||
'aliases.aliasName.description': '由你填写,仅新增入口,不会改名原模型',
|
||||
'aliases.aliasName.autoDescription': '根据原模型和已选选项自动生成,可按需修改;不会改名原模型',
|
||||
'aliases.aliasName.example': '例如 {model}-{effort}',
|
||||
'aliases.aliasName.selectFirst': '先选择原模型,再填写客户端使用的别名',
|
||||
'aliases.notSelected': '未选择模型',
|
||||
@@ -956,6 +962,10 @@ export const zhCN = {
|
||||
export type MessageKey = keyof typeof zhCN;
|
||||
|
||||
export const en: Record<MessageKey, string> = {
|
||||
'apiAccess.models.add': 'Add Custom Model',
|
||||
'apiAccess.models.namePlaceholder': 'Model name, for example gpt-4o',
|
||||
'apiAccess.models.aliasPlaceholder': 'Model alias (optional)',
|
||||
'apiAccess.models.remove': 'Remove model',
|
||||
'app.language': 'Language',
|
||||
'app.theme.light': 'Light mode',
|
||||
'app.theme.dark': 'Dark mode',
|
||||
@@ -1161,6 +1171,7 @@ export const en: Record<MessageKey, string> = {
|
||||
'aliases.error.emptyEffort': 'The reasoning effort cannot be empty',
|
||||
'aliases.error.emptyOptions': 'Set a reasoning effort or enable Fast',
|
||||
'aliases.error.unsupportedEffort': 'This model does not support a reasoning-effort override; only Fast can be used',
|
||||
'aliases.error.unsupportedFast': 'Fast is not available for this model',
|
||||
'aliases.created': 'Created {alias} with reasoning effort fixed to {effort}',
|
||||
'aliases.createdCombined': 'Created {alias} with reasoning effort {effort} and Fast enabled',
|
||||
'aliases.deleteConfirm': 'Delete the model alias “{alias}”?\nIts reasoning-effort and Fast overrides will also be removed.',
|
||||
@@ -1192,6 +1203,7 @@ export const en: Record<MessageKey, string> = {
|
||||
'aliases.customHelp': 'Enter only a level name supported by the target model',
|
||||
'aliases.aliasName.title': 'Model Alias',
|
||||
'aliases.aliasName.description': 'Adds another entry point without renaming the original model',
|
||||
'aliases.aliasName.autoDescription': 'Generated from the model and selected options; edit it as needed without renaming the original',
|
||||
'aliases.aliasName.example': 'For example: {model}-{effort}',
|
||||
'aliases.aliasName.selectFirst': 'Select the original model, then enter the alias clients will use',
|
||||
'aliases.notSelected': 'No model selected',
|
||||
|
||||
+84
-10
@@ -244,8 +244,12 @@ export const stripResponseFields = (record: Record<string, unknown>) => {
|
||||
|
||||
const mergeModelRecords = (current: unknown, selected: ModelOption[]) => {
|
||||
const existing = Array.isArray(current) ? current : [];
|
||||
return selected.map((model) => {
|
||||
const seen = new Set<string>();
|
||||
return selected.reduce<Record<string, unknown>[]>((models, model) => {
|
||||
const name = model.name.trim();
|
||||
const key = name.toLowerCase();
|
||||
if (!name || seen.has(key)) return models;
|
||||
seen.add(key);
|
||||
const matched = existing.find(
|
||||
(item) => isRecord(item) && readString(item, 'name').toLowerCase() === name.toLowerCase(),
|
||||
);
|
||||
@@ -255,8 +259,9 @@ const mergeModelRecords = (current: unknown, selected: ModelOption[]) => {
|
||||
if (alias && alias !== name) next.alias = alias;
|
||||
else delete next.alias;
|
||||
if (model.thinking) next.thinking = { ...model.thinking };
|
||||
return next;
|
||||
});
|
||||
models.push(next);
|
||||
return models;
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const exclusionsForModelSelection = (
|
||||
@@ -765,6 +770,10 @@ export function ApiAccessPage() {
|
||||
activeCategory,
|
||||
applyProviderPreset(activeCategory, nextDraft),
|
||||
);
|
||||
const preparedDraftForSave = {
|
||||
...preparedDraft,
|
||||
models: preparedDraft.models.filter((model) => model.name.trim()),
|
||||
};
|
||||
const baseUrlRequired = definition.openAi || definition.section === 'codex-api-key';
|
||||
const parsedApiKeys = preparedDraft.apiKey
|
||||
.split(/\r?\n/)
|
||||
@@ -801,7 +810,7 @@ export function ApiAccessPage() {
|
||||
setBusy(true);
|
||||
setError('');
|
||||
try {
|
||||
let draftToSave = { ...preparedDraft, baseUrl };
|
||||
let draftToSave = { ...preparedDraftForSave, baseUrl };
|
||||
if (definition.openAi && draftToSave.models.length === 0) {
|
||||
const fetchedModels = await fetchModels(
|
||||
'openai',
|
||||
@@ -1324,6 +1333,11 @@ function ApiProviderDialog({
|
||||
return Array.from(options.values());
|
||||
}, [discoveredModels, draft.models]);
|
||||
|
||||
const configuredModels = useMemo(
|
||||
() => draft.models.filter((model) => model.name.trim()),
|
||||
[draft.models],
|
||||
);
|
||||
|
||||
const visibleModelOptions = useMemo(() => {
|
||||
const query = modelSearch.trim().toLowerCase();
|
||||
if (!query) return modelOptions;
|
||||
@@ -1349,6 +1363,29 @@ function ApiProviderDialog({
|
||||
setDraft((current) => ({ ...current, [field]: value }));
|
||||
};
|
||||
|
||||
const updateModel = (index: number, patch: Partial<ModelOption>) => {
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
models: current.models.map((model, modelIndex) =>
|
||||
modelIndex === index ? { ...model, ...patch } : model,
|
||||
),
|
||||
}));
|
||||
};
|
||||
|
||||
const addModel = () => {
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
models: [...current.models, { name: '', alias: '' }],
|
||||
}));
|
||||
};
|
||||
|
||||
const removeModel = (index: number) => {
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
models: current.models.filter((_, modelIndex) => modelIndex !== index),
|
||||
}));
|
||||
};
|
||||
|
||||
const addThinkingLevel = () => {
|
||||
const level = thinkingLevelInput.trim().toLowerCase();
|
||||
if (!level) return;
|
||||
@@ -1400,7 +1437,11 @@ function ApiProviderDialog({
|
||||
{ ...draft, models: fetchedModels },
|
||||
).models;
|
||||
setDiscoveredModels(models);
|
||||
setSelectedModelNames(allModelSelectionForDiscovery(models));
|
||||
setSelectedModelNames(new Set(
|
||||
[...models, ...draft.models]
|
||||
.map((model) => model.name.trim().toLowerCase())
|
||||
.filter(Boolean),
|
||||
));
|
||||
if (!models.length) setModelError(t('apiAccess.error.noAvailableModels'));
|
||||
} catch (requestError) {
|
||||
setDiscoveredModels([]);
|
||||
@@ -1470,15 +1511,15 @@ function ApiProviderDialog({
|
||||
|
||||
const hasModelExclusions = activeSection !== 'openai-compatibility'
|
||||
&& Boolean(draft.excludedModelsText?.trim());
|
||||
const modelSummaryTitle = draft.models.length > 0
|
||||
? t('apiAccess.models.selected', { count: draft.models.length })
|
||||
const modelSummaryTitle = configuredModels.length > 0
|
||||
? t('apiAccess.models.selected', { count: configuredModels.length })
|
||||
: hasModelExclusions
|
||||
? t('apiAccess.models.restricted')
|
||||
: activeSection === 'openai-compatibility'
|
||||
? t('apiAccess.models.autoAll')
|
||||
: t('apiAccess.models.upstreamDefault');
|
||||
const modelSummaryDetail = draft.models.length > 0
|
||||
? draft.models.slice(0, 3).map((model) => model.name).join('、')
|
||||
const modelSummaryDetail = configuredModels.length > 0
|
||||
? configuredModels.slice(0, 3).map((model) => model.name).join('、')
|
||||
: hasModelExclusions
|
||||
? t('apiAccess.models.hiddenHint')
|
||||
: activeSection === 'openai-compatibility'
|
||||
@@ -1570,10 +1611,43 @@ function ApiProviderDialog({
|
||||
<RefreshCw size={15} />{t('apiAccess.models.fetch')}
|
||||
</button>
|
||||
</div>
|
||||
<div className={`model-config-summary ${draft.models.length || hasModelExclusions ? 'has-models' : ''}`}>
|
||||
<div className={`model-config-summary ${configuredModels.length || hasModelExclusions ? 'has-models' : ''}`}>
|
||||
<strong>{modelSummaryTitle}</strong>
|
||||
<span>{modelSummaryDetail}</span>
|
||||
</div>
|
||||
<div className="model-config-entries">
|
||||
{draft.models.map((model, index) => (
|
||||
<div className="model-config-entry" key={index}>
|
||||
<input
|
||||
value={model.name}
|
||||
onChange={(event) => updateModel(index, { name: event.currentTarget.value })}
|
||||
placeholder={t('apiAccess.models.namePlaceholder')}
|
||||
aria-label={t('apiAccess.models.namePlaceholder')}
|
||||
disabled={busy}
|
||||
/>
|
||||
<input
|
||||
value={model.alias ?? ''}
|
||||
onChange={(event) => updateModel(index, { alias: event.currentTarget.value })}
|
||||
placeholder={t('apiAccess.models.aliasPlaceholder')}
|
||||
aria-label={t('apiAccess.models.aliasPlaceholder')}
|
||||
disabled={busy}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button quiet danger"
|
||||
onClick={() => removeModel(index)}
|
||||
disabled={busy}
|
||||
title={t('apiAccess.models.remove')}
|
||||
aria-label={t('apiAccess.models.remove')}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button type="button" className="secondary-button compact-button model-config-add" onClick={addModel} disabled={busy}>
|
||||
<Plus size={14} />{t('apiAccess.models.add')}
|
||||
</button>
|
||||
</div>
|
||||
{modelError && !modelDiscoveryOpen ? <small className="model-picker-error">{modelError}</small> : null}
|
||||
</div>
|
||||
<label><span>{t('apiAccess.field.priority')}</span><input inputMode="numeric" value={draft.priority} onChange={(event) => updateTextField('priority', event.currentTarget.value.replace(/\D/g, ''))} placeholder={t('common.optional')} /></label>
|
||||
|
||||
@@ -53,6 +53,7 @@ type ThinkingAliasSource = {
|
||||
|
||||
type ModelAliasSource = ThinkingAliasSource & {
|
||||
supportsReasoning: boolean;
|
||||
supportsFast: boolean;
|
||||
};
|
||||
|
||||
const effortOptions = [
|
||||
@@ -96,16 +97,29 @@ export const combineModelAliasSources = (
|
||||
speedSources: ThinkingAliasSource[],
|
||||
): ModelAliasSource[] => {
|
||||
const reasoningSourceIds = new Set(thinkingSources.map((source) => source.id));
|
||||
const speedSourceIds = new Set(speedSources.map((source) => source.id));
|
||||
const sources = new Map<string, ModelAliasSource>();
|
||||
[...speedSources, ...thinkingSources].forEach((source) => {
|
||||
sources.set(source.id, {
|
||||
...source,
|
||||
supportsReasoning: reasoningSourceIds.has(source.id),
|
||||
supportsFast: speedSourceIds.has(source.id),
|
||||
});
|
||||
});
|
||||
return [...sources.values()];
|
||||
};
|
||||
|
||||
export const defaultModelAlias = (
|
||||
model: string | null | undefined,
|
||||
effort: string,
|
||||
fast: boolean,
|
||||
) => {
|
||||
const normalizedModel = model?.trim() ?? '';
|
||||
const normalizedEffort = effort.trim().toLowerCase();
|
||||
if (!normalizedModel || (!normalizedEffort && !fast)) return '';
|
||||
return `${normalizedModel}${normalizedEffort ? `-${normalizedEffort}` : ''}${fast ? '-fast' : ''}`;
|
||||
};
|
||||
|
||||
export const thinkingAliasSourceKindLabel = (kind: string) => {
|
||||
if (kind === 'codex-oauth') return 'Codex OAuth';
|
||||
if (kind === 'codex-api') return 'Codex API';
|
||||
@@ -138,6 +152,7 @@ export function ThinkingAliasesPage() {
|
||||
const [modelPickerOpen, setModelPickerOpen] = useState(false);
|
||||
const [activeSourceIndex, setActiveSourceIndex] = useState(0);
|
||||
const modelPickerRef = useRef<HTMLDivElement>(null);
|
||||
const generatedAliasRef = useRef('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [busyAlias, setBusyAlias] = useState('');
|
||||
const [busyAction, setBusyAction] = useState<'create' | 'delete' | ''>('');
|
||||
@@ -236,9 +251,14 @@ export function ThinkingAliasesPage() {
|
||||
setSelectedSourceId(source.id);
|
||||
if (!source.supportsReasoning) {
|
||||
setEffort('');
|
||||
setFastEnabled(true);
|
||||
setCustomEffortOpen(false);
|
||||
setFastEnabled(source.supportsFast);
|
||||
} else {
|
||||
setEffort((current) => current || 'xhigh');
|
||||
setFastEnabled(false);
|
||||
}
|
||||
setCustomEffortOpen(false);
|
||||
setAlias('');
|
||||
generatedAliasRef.current = '';
|
||||
setModelPickerOpen(false);
|
||||
setSearch('');
|
||||
};
|
||||
@@ -274,11 +294,22 @@ export function ThinkingAliasesPage() {
|
||||
};
|
||||
|
||||
const normalizedEffort = effort.trim().toLowerCase();
|
||||
const defaultAlias = defaultModelAlias(selectedSource?.model, normalizedEffort, fastEnabled);
|
||||
const customEffortSelected = Boolean(
|
||||
normalizedEffort
|
||||
&& !effortOptions.some((option) => option.value === normalizedEffort),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setAlias((current) => {
|
||||
const currentValue = current.trim();
|
||||
const canReplace = !currentValue || currentValue === generatedAliasRef.current;
|
||||
if (!canReplace) return current;
|
||||
generatedAliasRef.current = defaultAlias;
|
||||
return defaultAlias;
|
||||
});
|
||||
}, [defaultAlias]);
|
||||
|
||||
const createAlias = async () => {
|
||||
if (!selectedSource) {
|
||||
setError(t('aliases.error.selectModel'));
|
||||
@@ -293,6 +324,10 @@ export function ThinkingAliasesPage() {
|
||||
setError(t('aliases.error.emptyOptions'));
|
||||
return;
|
||||
}
|
||||
if (fastEnabled && !selectedSource.supportsFast) {
|
||||
setError(t('aliases.error.unsupportedFast'));
|
||||
return;
|
||||
}
|
||||
if (normalizedEffort && !selectedSource.supportsReasoning) {
|
||||
setError(t('aliases.error.unsupportedEffort'));
|
||||
return;
|
||||
@@ -549,23 +584,29 @@ export function ThinkingAliasesPage() {
|
||||
<strong>{t('aliases.fast.title')}</strong>
|
||||
<span>{t('aliases.fast.description')}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={`thinking-speed-toggle${fastEnabled ? ' active' : ''}`}
|
||||
aria-pressed={fastEnabled}
|
||||
onClick={() => setFastEnabled((current) => !current)}
|
||||
disabled={Boolean(busyAlias)}
|
||||
>
|
||||
<span>Fast</span>
|
||||
<small>{fastEnabled ? t('aliases.fast.enabled') : t('aliases.fast.disabled')}</small>
|
||||
</button>
|
||||
<label className={`thinking-fast-option${fastEnabled ? ' active' : ''}`}>
|
||||
<span className="thinking-fast-option-copy">
|
||||
<span><Zap size={15} /> Fast</span>
|
||||
<small>{fastEnabled ? t('aliases.fast.enabled') : t('aliases.fast.disabled')}</small>
|
||||
</span>
|
||||
<span className="switch-control thinking-fast-switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={fastEnabled}
|
||||
onChange={(event) => setFastEnabled(event.currentTarget.checked)}
|
||||
disabled={Boolean(busyAlias) || !selectedSource || !selectedSource.supportsFast}
|
||||
aria-label={t('aliases.fast.title')}
|
||||
/>
|
||||
<span className="switch-track" />
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="thinking-alias-section-divider" aria-hidden="true" />
|
||||
|
||||
<div className="thinking-alias-field">
|
||||
<div className="thinking-field-heading">
|
||||
<strong>{t('aliases.aliasName.title')}</strong>
|
||||
<span>{t('aliases.aliasName.description')}</span>
|
||||
<span>{t('aliases.aliasName.autoDescription')}</span>
|
||||
</div>
|
||||
<input
|
||||
id="thinking-alias-name"
|
||||
|
||||
+70
-13
@@ -2895,6 +2895,32 @@ textarea:focus-visible {
|
||||
font-size: var(--font-size-meta);
|
||||
}
|
||||
|
||||
.model-config-entries {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.model-config-entry {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.model-config-entry input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.model-config-entry .icon-button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.model-config-add {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.model-picker-error {
|
||||
color: var(--theme-9f2d20);
|
||||
font-size: var(--font-size-meta);
|
||||
@@ -4280,39 +4306,70 @@ textarea:focus-visible {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.thinking-speed-toggle {
|
||||
.thinking-fast-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
min-height: 42px;
|
||||
padding: 7px 10px;
|
||||
min-height: 48px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--theme-d5d2cb);
|
||||
border-radius: 7px;
|
||||
background: var(--theme-fff);
|
||||
color: var(--theme-7a746c);
|
||||
cursor: pointer;
|
||||
transition: border-color 140ms ease, background 140ms ease, color 140ms ease;
|
||||
}
|
||||
|
||||
.thinking-speed-toggle span {
|
||||
font-size: var(--font-size-section);
|
||||
font-weight: 800;
|
||||
.thinking-fast-option:hover {
|
||||
border-color: var(--theme-d7a447);
|
||||
}
|
||||
|
||||
.thinking-speed-toggle small {
|
||||
color: var(--theme-9a938a);
|
||||
font-size: var(--font-size-label);
|
||||
}
|
||||
|
||||
.thinking-speed-toggle.active {
|
||||
.thinking-fast-option.active {
|
||||
border-color: var(--theme-d7a447);
|
||||
background: var(--theme-fff8eb);
|
||||
color: var(--theme-805a24);
|
||||
}
|
||||
|
||||
.thinking-speed-toggle.active small {
|
||||
.thinking-fast-option-copy {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.thinking-fast-option-copy > span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: var(--font-size-section);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.thinking-fast-option-copy small {
|
||||
color: var(--theme-9a938a);
|
||||
font-size: var(--font-size-label);
|
||||
}
|
||||
|
||||
.thinking-fast-option.active .thinking-fast-option-copy small {
|
||||
color: var(--theme-805a24);
|
||||
}
|
||||
|
||||
.thinking-fast-option:has(input:disabled) {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.thinking-fast-switch {
|
||||
width: 42px;
|
||||
height: 23px;
|
||||
}
|
||||
|
||||
.thinking-fast-switch input:checked + .switch-track {
|
||||
border-color: var(--theme-d7a447);
|
||||
background: var(--theme-d7a447);
|
||||
}
|
||||
|
||||
.thinking-effort-custom {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 30px;
|
||||
|
||||
@@ -18,6 +18,22 @@ import {
|
||||
stripResponseFields,
|
||||
} from '../src/pages/ApiAccessPage';
|
||||
|
||||
it('saves non-empty custom model names and removes duplicate or blank entries', () => {
|
||||
const result = buildProviderRecord('openai-compatibility', {
|
||||
name: 'custom-provider',
|
||||
apiKey: 'provider-key',
|
||||
baseUrl: 'https://api.example.com',
|
||||
priority: '',
|
||||
models: [
|
||||
{ name: ' custom-model ', alias: ' Custom Alias ' },
|
||||
{ name: ' ' },
|
||||
{ name: 'CUSTOM-MODEL', alias: 'duplicate' },
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.models).toEqual([{ name: 'custom-model', alias: 'Custom Alias' }]);
|
||||
});
|
||||
|
||||
describe('API 接入配置合并', () => {
|
||||
it('固定使用 Codex、OpenAI、DeepSeek、Claude、Gemini 顺序且不包含 Vertex', () => {
|
||||
expect(providerSectionOrder).toEqual([
|
||||
|
||||
@@ -2,9 +2,19 @@ import { describe, expect, test } from 'bun:test';
|
||||
import {
|
||||
combineModelAliasEntries,
|
||||
combineModelAliasSources,
|
||||
defaultModelAlias,
|
||||
thinkingAliasSourceKindLabel,
|
||||
} from '../src/pages/ThinkingAliasesPage';
|
||||
|
||||
describe('模型别名默认名称', () => {
|
||||
test('按思考强度和 Fast 选项生成可编辑的默认名称', () => {
|
||||
expect(defaultModelAlias('gpt-5.6-sol', 'XHigh', false)).toBe('gpt-5.6-sol-xhigh');
|
||||
expect(defaultModelAlias('gpt-5.6-sol', '', true)).toBe('gpt-5.6-sol-fast');
|
||||
expect(defaultModelAlias('gpt-5.6-sol', 'xhigh', true)).toBe('gpt-5.6-sol-xhigh-fast');
|
||||
expect(defaultModelAlias('gpt-5.6-sol', '', false)).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('思考别名', () => {
|
||||
test('区分同名模型的接入来源', () => {
|
||||
expect(thinkingAliasSourceKindLabel('codex-oauth')).toBe('Codex OAuth');
|
||||
@@ -73,6 +83,8 @@ describe('统一模型别名列表', () => {
|
||||
|
||||
expect(sources).toHaveLength(2);
|
||||
expect(sources.find((source) => source.id === 'reasoning-and-fast')?.supportsReasoning).toBe(true);
|
||||
expect(sources.find((source) => source.id === 'reasoning-and-fast')?.supportsFast).toBe(true);
|
||||
expect(sources.find((source) => source.id === 'fast-only')?.supportsReasoning).toBe(false);
|
||||
expect(sources.find((source) => source.id === 'fast-only')?.supportsFast).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user