项目文件夹

文件
wehub-resource-sync e9a2f726c9
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / test (3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:29:51 +08:00

1030 行
101 KiB
HTML

<!-- Model Settings Modal -->
<div x-show="showModelSettingsModal" x-cloak
class="fixed inset-0 z-50 overflow-y-auto"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0">
<!-- Backdrop -->
<div class="fixed inset-0 bg-black/50" @click="showModelSettingsModal = false"></div>
<!-- Modal -->
<div class="flex min-h-full items-center justify-center p-4">
<div class="relative bg-white rounded-2xl sm:rounded-3xl shadow-2xl w-full max-w-4xl p-4 sm:p-8"
@click.stop
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95">
<!-- Header -->
<div class="flex items-center justify-between mb-6">
<div>
<h3 class="text-xs font-bold uppercase tracking-widest text-neutral-400 mb-1">{{ t('modal.model_settings.section_label') }}</h3>
<h4 class="text-xl font-bold tracking-tight text-neutral-900" x-text="selectedModel?.display_name || selectedModel?.name || selectedModel?.id"></h4>
</div>
<button @click="showModelSettingsModal = false"
class="p-2 text-neutral-400 hover:text-neutral-600 hover:bg-neutral-100 rounded-full transition-all">
<i data-lucide="x" class="w-5 h-5"></i>
</button>
</div>
<!-- Profiles / Templates / Presets section -->
<div class="mb-6 p-4 bg-neutral-50 rounded-xl border border-neutral-200"
x-show="!modelSettings.is_diffusion_model" x-cloak
@input.stop="computeDrift()"
@change.stop="computeDrift()">
<div class="flex items-center justify-between mb-3 gap-3 flex-wrap">
<h3 class="text-xs font-bold uppercase tracking-widest text-neutral-400">{{ t('modal.model_settings.profiles.section_label') }}</h3>
<div class="flex items-center gap-2">
<span x-show="profileError" x-cloak class="text-xs text-red-600" x-text="profileError"></span>
<!-- Scope toggle -->
<div class="flex items-center gap-1 bg-neutral-100 rounded-lg p-0.5">
<button type="button" @click="setScope('preset')"
:class="profileScope === 'preset' ? 'bg-white text-neutral-900 shadow-sm' : 'text-neutral-500 hover:text-neutral-700'"
class="px-3 py-1 text-xs font-medium rounded-md transition-all">
{{ t('modal.model_settings.profiles.scope_preset') }}
</button>
<button type="button" @click="setScope('global')"
:class="profileScope === 'global' ? 'bg-white text-neutral-900 shadow-sm' : 'text-neutral-500 hover:text-neutral-700'"
class="px-3 py-1 text-xs font-medium rounded-md transition-all">
{{ t('modal.model_settings.profiles.scope_global') }}
</button>
<button type="button" @click="setScope('model')"
:class="profileScope === 'model' ? 'bg-white text-neutral-900 shadow-sm' : 'text-neutral-500 hover:text-neutral-700'"
class="px-3 py-1 text-xs font-medium rounded-md transition-all">
{{ t('modal.model_settings.profiles.scope_model') }}
</button>
</div>
<!-- Refresh button (only in Preset scope) -->
<button type="button" x-show="profileScope === 'preset'"
@click="refreshPresets()"
:disabled="refreshingPresets"
@mouseenter="showTip($event.currentTarget, 'Fetch latest presets from omlx.ai')"
@mouseleave="hideTip()"
class="p-1 rounded-full text-neutral-400 hover:text-neutral-700 hover:bg-neutral-100 transition-all disabled:opacity-50">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
:class="refreshingPresets ? 'animate-spin' : ''" class="w-3.5 h-3.5">
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/>
<path d="M21 3v5h-5"/>
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/>
<path d="M3 21v-5h5"/>
</svg>
</button>
</div>
</div>
<!-- Preset scope: read-only pills with hover tooltip.
Inline style used so the height clamp is immune to any tailwind
cache / specificity issue that was previously letting 3+ rows show. -->
<div x-show="profileScope === 'preset'" x-cloak
style="max-height: 72px; overflow-y: auto;"
class="pills-scrollbar">
<div class="flex flex-wrap items-center gap-2">
<template x-for="p in presets" :key="p.name">
<button type="button" @click="applyPresetToForm(p)"
@mouseenter="showTip($event.currentTarget, p.description)"
@mouseleave="hideTip()"
class="px-2.5 py-1 text-xs rounded-full border border-blue-200 bg-blue-50 text-blue-700 hover:bg-blue-100 transition-all"
x-text="p.display_name || p.name"></button>
</template>
<span x-show="presets.length === 0" x-cloak class="text-xs text-neutral-400 italic">no presets available</span>
</div>
</div>
<!-- Global scope: templates row -->
<div x-show="profileScope === 'global'" x-cloak
style="max-height: 72px; overflow-y: auto;"
class="pills-scrollbar">
<div class="flex flex-wrap items-center gap-2">
<template x-for="tmpl in templates" :key="tmpl.name">
<div class="inline-flex items-center gap-1">
<button type="button" @click="applyTemplateToForm(tmpl)"
class="px-2.5 py-1 text-xs rounded-full border border-blue-200 bg-blue-50 text-blue-700 hover:bg-blue-100 transition-all"
x-text="tmpl.display_name || tmpl.name"></button>
<button type="button" @click="editingTemplate = tmpl.name"
class="p-0.5 text-neutral-400 hover:text-neutral-700 hover:bg-neutral-200 rounded transition-colors"
:title="t('modal.model_settings.profiles.update_from_form')">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-3 h-3"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>
</button>
<button type="button" @click="templateDeleteConfirm = tmpl.name"
class="p-0.5 text-neutral-400 hover:text-red-500 hover:bg-red-50 rounded transition-colors"
:title="t('modal.model_settings.profiles.delete')">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-3 h-3"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>
</button>
<span x-show="templateDeleteConfirm === tmpl.name" x-cloak class="flex items-center gap-1">
<button type="button" @click="deleteTemplate(tmpl.name)"
class="px-1.5 py-0.5 text-[10px] text-white bg-red-500 rounded">{{ t('models.manager.delete_yes') }}</button>
<button type="button" @click="templateDeleteConfirm = null"
class="px-1.5 py-0.5 text-[10px] text-neutral-600">{{ t('models.manager.delete_no') }}</button>
</span>
</div>
</template>
<button type="button" @click="showNewTemplateForm = true; newTemplate = {name:'',display_name:'',description:''}"
class="px-2.5 py-1 text-xs rounded-full border border-dashed border-neutral-300 text-neutral-500 hover:text-neutral-900 hover:border-neutral-500 transition-all">
{{ t('modal.model_settings.profiles.new_template') }}
</button>
</div>
</div>
<!-- New template inline form (global scope) -->
<div x-show="profileScope === 'global' && showNewTemplateForm" x-cloak class="mt-3 p-3 bg-white border border-neutral-200 rounded-lg space-y-2">
<input type="text" x-model="newTemplate.display_name" :placeholder="t('modal.model_settings.profiles.display_name_label')"
class="w-full px-3 py-1.5 border border-neutral-200 rounded-lg text-xs">
<input type="text" x-model="newTemplate.description" :placeholder="t('modal.model_settings.profiles.description_label')"
class="w-full px-3 py-1.5 border border-neutral-200 rounded-lg text-xs">
<div class="flex justify-end gap-2">
<button type="button" @click="showNewTemplateForm = false"
class="px-3 py-1 text-xs text-neutral-600 hover:bg-neutral-100 rounded">{{ t('modal.model_settings.profiles.cancel') }}</button>
<button type="button" @click="createTemplate()"
class="px-3 py-1 text-xs bg-neutral-900 text-white rounded hover:bg-neutral-800">{{ t('modal.model_settings.profiles.save') }}</button>
</div>
</div>
<!-- Edit template inline dialog (global scope) -->
<template x-for="tmpl in templates" :key="'edit-'+tmpl.name">
<div x-show="profileScope === 'global' && editingTemplate === tmpl.name" x-cloak class="mt-3 p-3 bg-white border border-neutral-200 rounded-lg space-y-2">
<input type="text" :value="tmpl.display_name" @input="tmpl.display_name = $event.target.value"
class="w-full px-3 py-1.5 border border-neutral-200 rounded-lg text-xs">
<div class="flex justify-between gap-2">
<button type="button"
@click="updateTemplate(tmpl.name, {display_name: tmpl.display_name, settings: formValuesForTemplate()})"
class="px-3 py-1 text-xs bg-neutral-900 text-white rounded hover:bg-neutral-800">
{{ t('modal.model_settings.profiles.update_from_form') }}
</button>
<button type="button" @click="editingTemplate = null"
class="px-3 py-1 text-xs text-neutral-600 hover:bg-neutral-100 rounded">
{{ t('modal.model_settings.profiles.cancel') }}
</button>
</div>
</div>
</template>
<!-- Model scope: per-model profiles -->
<div x-show="profileScope === 'model'" x-cloak
style="max-height: 72px; overflow-y: auto;"
class="pills-scrollbar">
<div class="flex flex-wrap items-center gap-2">
<template x-for="p in profiles" :key="p.name">
<div class="inline-flex items-center gap-1">
<button type="button" @click="applyProfileToForm(p)"
@mouseenter="showTip($event.currentTarget, profileTooltip(p))"
@mouseleave="hideTip()"
:class="activeProfileName === p.name
? 'bg-emerald-500 text-white border border-emerald-500'
: 'bg-emerald-50 text-emerald-700 border border-emerald-200 hover:bg-emerald-100'"
class="px-2.5 py-1 text-xs rounded-full transition-all flex items-center gap-1">
<span x-show="p.expose_as_model"
class="px-1 py-0.5 text-[9px] font-bold rounded bg-white/70"
:class="p.has_engine_fields ? 'text-amber-700' : 'text-emerald-700'">API</span>
<span x-text="p.display_name || p.name"></span>
<span x-show="activeProfileName === p.name && profilesDrift"
class="w-1.5 h-1.5 rounded-full bg-white"
:title="t('modal.model_settings.profiles.drift_hint')"></span>
</button>
<button type="button"
@click="editingProfile = p.name; p._editDisplayName = p.display_name || p.name; p._editApiName = p.api_name || p.name; p._editDescription = p.description || ''; p._editExposeAsModel = !!p.expose_as_model; profileError = ''"
class="p-0.5 text-neutral-400 hover:text-neutral-700 hover:bg-neutral-200 rounded transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-3 h-3"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>
</button>
<button type="button" @click="profileDeleteConfirm = p.name"
class="p-0.5 text-neutral-400 hover:text-red-500 hover:bg-red-50 rounded transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-3 h-3"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>
</button>
<span x-show="profileDeleteConfirm === p.name" x-cloak class="flex items-center gap-1">
<button type="button" @click="deleteProfile(p.name)"
class="px-1.5 py-0.5 text-[10px] text-white bg-red-500 rounded">{{ t('models.manager.delete_yes') }}</button>
<button type="button" @click="profileDeleteConfirm = null"
class="px-1.5 py-0.5 text-[10px] text-neutral-600">{{ t('models.manager.delete_no') }}</button>
</span>
</div>
</template>
<button type="button"
@click="showNewProfileForm = true; profileError = ''; newProfile = {display_name:'',api_name:'',api_name_touched:false,description:'',also_as_template:false}"
class="px-2.5 py-1 text-xs rounded-full border border-dashed border-neutral-300 text-neutral-500 hover:text-neutral-900 hover:border-neutral-500 transition-all">
{{ t('modal.model_settings.profiles.new_profile') }}
</button>
</div>
</div>
<!-- New profile inline form (model scope) -->
<div x-show="profileScope === 'model' && showNewProfileForm" x-cloak class="mt-3 p-3 bg-white border border-neutral-200 rounded-lg space-y-2">
<input type="text" x-model="newProfile.display_name" :placeholder="t('modal.model_settings.profiles.display_name_label')"
@input="if (!newProfile.api_name_touched) newProfile.api_name = slugifyProfileApiName(newProfile.display_name)"
class="w-full px-3 py-1.5 border border-neutral-200 rounded-lg text-xs">
<input type="text" x-model="newProfile.api_name" @input="newProfile.api_name_touched = true" :placeholder="t('modal.model_settings.profiles.name_placeholder')"
class="w-full px-3 py-1.5 border border-neutral-200 rounded-lg text-xs font-mono">
<input type="text" x-model="newProfile.description" :placeholder="t('modal.model_settings.profiles.description_label')"
class="w-full px-3 py-1.5 border border-neutral-200 rounded-lg text-xs">
<div class="flex justify-end gap-2">
<button type="button" @click="showNewProfileForm = false"
class="px-3 py-1 text-xs text-neutral-600 hover:bg-neutral-100 rounded">{{ t('modal.model_settings.profiles.cancel') }}</button>
<button type="button" @click="createProfile()"
class="px-3 py-1 text-xs bg-neutral-900 text-white rounded hover:bg-neutral-800">{{ t('modal.model_settings.profiles.save') }}</button>
</div>
</div>
<!-- Edit profile inline dialog (model scope) -->
<template x-for="p in profiles" :key="'edit-'+p.name">
<div x-show="profileScope === 'model' && editingProfile === p.name" x-cloak class="mt-3 p-3 bg-white border border-neutral-200 rounded-lg space-y-2">
<div class="block">
<span class="block mb-1 text-[10px] font-semibold uppercase tracking-wide text-neutral-500">{{ t('modal.model_settings.profiles.display_name_label') }}</span>
<input type="text" :value="p._editDisplayName ?? p.display_name" @input="p._editDisplayName = $event.target.value"
:placeholder="t('modal.model_settings.profiles.display_name_label')"
class="w-full px-3 py-1.5 border border-neutral-200 rounded-lg text-xs">
</div>
<div class="block">
<span class="block mb-1 text-[10px] font-semibold uppercase tracking-wide text-neutral-500">{{ t('modal.model_settings.profiles.name_placeholder') }}</span>
<div class="flex gap-2">
<input type="text" :value="p._editApiName ?? p.api_name" @input="p._editApiName = $event.target.value"
:placeholder="t('modal.model_settings.profiles.name_placeholder')"
class="min-w-0 flex-1 px-3 py-1.5 border border-neutral-200 rounded-lg text-xs font-mono">
<button type="button"
@click="p._editExposeAsModel = !(p._editExposeAsModel ?? p.expose_as_model)"
:title="t('modal.model_settings.profiles.expose_as_model')"
:class="(p._editExposeAsModel ?? p.expose_as_model)
? 'bg-emerald-600 text-white border-emerald-600'
: 'bg-white text-neutral-500 border-neutral-200 hover:text-neutral-900 hover:border-neutral-400'"
class="shrink-0 inline-flex items-center gap-1 px-2.5 py-1.5 border rounded-lg text-[10px] font-bold transition-colors">
API
<span x-text="(p._editExposeAsModel ?? p.expose_as_model) ? 'ON' : 'OFF'"></span>
</button>
</div>
</div>
<div class="block">
<span class="block mb-1 text-[10px] font-semibold uppercase tracking-wide text-neutral-500">{{ t('modal.model_settings.profiles.description_label') }}</span>
<input type="text" :value="p._editDescription ?? p.description" @input="p._editDescription = $event.target.value"
:placeholder="t('modal.model_settings.profiles.description_label')"
class="w-full px-3 py-1.5 border border-neutral-200 rounded-lg text-xs">
</div>
<div class="flex justify-between gap-2 flex-wrap">
<div class="flex gap-2 flex-wrap">
<button type="button"
@click="updateProfileFromEdit(p)"
class="px-3 py-1 text-xs bg-neutral-900 text-white rounded hover:bg-neutral-800">
{{ t('modal.model_settings.profiles.save') }}
</button>
<button type="button"
@click="updateProfileSettingsFromForm(p)"
class="px-3 py-1 text-xs border border-neutral-300 text-neutral-700 rounded hover:bg-neutral-100">
{{ t('modal.model_settings.profiles.update_from_form') }}
</button>
<button type="button" x-show="p.source_template"
@click="(() => { const tmpl = templates.find(t => t.name === p.source_template); if (tmpl) { applyTemplateToForm(tmpl); updateProfile(p.name, {settings: formValuesForProfile()}); } })()"
class="px-3 py-1 text-xs border border-neutral-300 text-neutral-700 rounded hover:bg-neutral-100">
{{ t('modal.model_settings.profiles.resync_template') }}
</button>
</div>
<button type="button" @click="editingProfile = null"
class="px-3 py-1 text-xs text-neutral-600 hover:bg-neutral-100 rounded">
{{ t('modal.model_settings.profiles.cancel') }}
</button>
</div>
</div>
</template>
</div>
<!-- Two-column layout -->
<div class="flex flex-col lg:flex-row">
<!-- Left: Basic Settings -->
<div class="w-full lg:w-3/5 lg:pr-6 lg:border-r border-neutral-200">
<h3 class="text-xs font-bold uppercase tracking-widest text-neutral-400 mb-5">{{ t('modal.model_settings.basic_label') }}</h3>
<div class="space-y-5">
<!-- Row 1: Model Alias / Model Type / Reasoning Parser -->
<div class="grid grid-cols-1 gap-4" :class="reasoningParsers.length > 0 && !modelSettings.is_diffusion_model ? 'sm:grid-cols-3' : 'sm:grid-cols-2'">
<div>
<label class="block text-xs font-bold uppercase tracking-wider text-neutral-500 mb-2">{{ t('modal.model_settings.model_alias') }}</label>
<input type="text" x-model="modelSettings.model_alias"
:placeholder="selectedModel?.id"
class="w-full px-4 py-2.5 border border-neutral-200 rounded-xl text-sm focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all">
</div>
<div>
<label class="block text-xs font-bold uppercase tracking-wider text-neutral-500 mb-2">{{ t('modal.model_settings.model_type') }}</label>
<select x-model="modelSettings.model_type_override"
class="w-full px-4 py-2.5 border border-neutral-200 rounded-xl text-sm focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all bg-white">
<option value="">{{ t('modal.model_settings.model_type_auto') }}</option>
<option value="llm">LLM</option>
<option value="vlm">VLM</option>
<option value="embedding">Embedding</option>
<option value="reranker">Reranker</option>
<option value="audio_stt">Audio STT</option>
<option value="audio_tts">Audio TTS</option>
<option value="audio_sts">Audio STS</option>
</select>
</div>
<div x-show="reasoningParsers.length > 0 && !modelSettings.is_diffusion_model">
<label class="block text-xs font-bold uppercase tracking-wider text-neutral-500 mb-2">Reasoning Parser</label>
<select x-model="modelSettings.reasoning_parser"
class="w-full px-4 py-2.5 border border-neutral-200 rounded-xl text-sm focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all bg-white">
<option value="">None</option>
<template x-for="p in reasoningParsers" :key="p.value">
<option :value="p.value" x-text="p.label + ' (' + p.models.join(', ') + ')'"></option>
</template>
</select>
</div>
</div>
<!-- Sampling settings (LLM/VLM only) -->
<template x-if="!selectedModel?.model_type || selectedModel.model_type === 'llm' || selectedModel.model_type === 'vlm'">
<div class="space-y-5">
<!-- Row 2: Ctx Window / Max Tokens / Temperature -->
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4"
x-show="!modelSettings.is_diffusion_model" x-cloak>
<div>
<label class="block text-xs font-bold uppercase tracking-wider text-neutral-500 mb-2">{{ t('modal.model_settings.max_context_window') }}</label>
<input type="number" x-model.number="modelSettings.max_context_window" placeholder="{{ t('modal.model_settings.placeholder_default') }}" min="1"
class="w-full px-4 py-2.5 border border-neutral-200 rounded-xl text-sm focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all">
</div>
<div>
<label class="block text-xs font-bold uppercase tracking-wider text-neutral-500 mb-2">{{ t('modal.model_settings.max_tokens') }}</label>
<input type="number" x-model.number="modelSettings.max_tokens" placeholder="{{ t('modal.model_settings.placeholder_default') }}"
class="w-full px-4 py-2.5 border border-neutral-200 rounded-xl text-sm focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all">
</div>
<div>
<label class="block text-xs font-bold uppercase tracking-wider text-neutral-500 mb-2">{{ t('modal.model_settings.temperature') }}</label>
<input type="number" x-model.number="modelSettings.temperature" step="0.1" min="0" max="2" placeholder="{{ t('modal.model_settings.placeholder_default') }}"
class="w-full px-4 py-2.5 border border-neutral-200 rounded-xl text-sm focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all">
</div>
</div>
<!-- Row 3: Top P / Top K / Min P -->
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div>
<label class="block text-xs font-bold uppercase tracking-wider text-neutral-500 mb-2">{{ t('modal.model_settings.top_p') }}</label>
<input type="number" x-model.number="modelSettings.top_p" step="0.05" min="0" max="1" placeholder="{{ t('modal.model_settings.placeholder_default') }}"
class="w-full px-4 py-2.5 border border-neutral-200 rounded-xl text-sm focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all">
</div>
<div>
<label class="block text-xs font-bold uppercase tracking-wider text-neutral-500 mb-2">{{ t('modal.model_settings.top_k') }}</label>
<input type="number" x-model.number="modelSettings.top_k" min="0" placeholder="{{ t('modal.model_settings.placeholder_default') }}"
class="w-full px-4 py-2.5 border border-neutral-200 rounded-xl text-sm focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all">
</div>
<div>
<label class="block text-xs font-bold uppercase tracking-wider text-neutral-500 mb-2">{{ t('modal.model_settings.min_p') }}</label>
<input type="number" x-model.number="modelSettings.min_p" step="0.01" min="0" max="1" placeholder="{{ t('modal.model_settings.placeholder_default') }}"
class="w-full px-4 py-2.5 border border-neutral-200 rounded-xl text-sm focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all">
</div>
</div>
<!-- Row 4: Repetition Penalty / Presence Penalty -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4"
x-show="!modelSettings.is_diffusion_model" x-cloak>
<div>
<label class="block text-xs font-bold uppercase tracking-wider text-neutral-500 mb-2">{{ t('modal.model_settings.repetition_penalty') }}</label>
<input type="number" x-model.number="modelSettings.repetition_penalty" step="0.05" min="0" placeholder="{{ t('modal.model_settings.placeholder_default') }}"
class="w-full px-4 py-2.5 border border-neutral-200 rounded-xl text-sm focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all">
</div>
<div>
<label class="block text-xs font-bold uppercase tracking-wider text-neutral-500 mb-2">{{ t('modal.model_settings.presence_penalty') }}</label>
<input type="number" x-model.number="modelSettings.presence_penalty" step="0.05" min="-2" max="2" placeholder="{{ t('modal.model_settings.placeholder_default') }}"
class="w-full px-4 py-2.5 border border-neutral-200 rounded-xl text-sm focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all">
</div>
</div>
</div>
</template>
<!-- TTL (all model types) -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-xs font-bold uppercase tracking-wider text-neutral-500 mb-2">{{ t('modal.model_settings.ttl_seconds') }}</label>
<input type="number" x-model.number="modelSettings.ttl_seconds"
:placeholder="ttlPlaceholder"
:disabled="selectedModel?.pinned"
min="0" step="60"
:class="selectedModel?.pinned ? 'bg-neutral-100 text-neutral-400 cursor-not-allowed' : ''"
class="w-full px-4 py-2.5 border border-neutral-200 rounded-xl text-sm focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all">
</div>
<div class="flex items-end"
x-show="!selectedModel?.model_type || selectedModel.model_type === 'llm' || selectedModel.model_type === 'vlm'">
<button type="button" @click="loadGenerationDefaults"
:disabled="loadingGenDefaults"
class="w-full px-4 py-2.5 text-sm font-medium text-neutral-700 bg-neutral-100 hover:bg-neutral-200 rounded-xl border border-neutral-200 transition-all disabled:opacity-50">
<span x-show="!loadingGenDefaults">{{ t('modal.model_settings.load_defaults') }}</span>
<span x-show="loadingGenDefaults">{{ t('modal.model_settings.loading_defaults') }}</span>
</button>
</div>
</div>
<p class="text-xs text-neutral-400 text-center"
x-show="!selectedModel?.model_type || selectedModel.model_type === 'llm' || selectedModel.model_type === 'vlm'">{{ t('modal.model_settings.empty_hint') }}</p>
</div>
</div>
<!-- Right: Advanced Settings -->
<div class="w-full lg:w-2/5 lg:pl-6 mt-6 lg:mt-0 pt-6 lg:pt-0 border-t lg:border-t-0 border-neutral-200">
<h3 class="text-xs font-bold uppercase tracking-widest text-neutral-400 mb-5">{{ t('modal.model_settings.advanced_label') }}</h3>
<div class="overflow-y-auto max-h-[28rem] space-y-4 pr-1"
x-show="!selectedModel?.model_type || selectedModel.model_type === 'llm' || selectedModel.model_type === 'vlm'">
<!-- Enable Thinking -->
<div class="p-4 bg-neutral-50 rounded-xl space-y-3"
x-show="!modelSettings.is_diffusion_model" x-cloak>
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<span class="text-sm font-medium text-neutral-700">{{ t('modal.model_settings.enable_thinking') }}</span>
<p class="text-xs text-neutral-500 mt-0.5">{{ t('modal.model_settings.enable_thinking_hint') }}</p>
</div>
<button type="button" @click="
const effective = modelSettings.enable_thinking != null ? modelSettings.enable_thinking : (modelSettings.thinking_default || false);
modelSettings.enable_thinking = !effective;
"
:class="(modelSettings.enable_thinking != null ? modelSettings.enable_thinking : (modelSettings.thinking_default || false)) ? 'bg-black' : 'bg-neutral-200'"
class="relative flex-shrink-0 w-11 h-6 mt-0.5 rounded-full transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black">
<span :class="(modelSettings.enable_thinking != null ? modelSettings.enable_thinking : (modelSettings.thinking_default || false)) ? 'translate-x-5' : 'translate-x-0'"
class="block w-5 h-5 bg-white rounded-full shadow-sm transform transition-transform duration-300 absolute top-0.5 left-0.5"></span>
</button>
</div>
<p x-show="modelSettings.enable_thinking == null && modelSettings.thinking_default != null"
class="text-xs text-neutral-400" x-text="'{{ t('modal.model_settings.enable_thinking_default') }}'"></p>
<p x-show="modelSettings.enable_thinking != null && modelSettings.thinking_default != null && modelSettings.enable_thinking !== modelSettings.thinking_default"
class="text-xs text-amber-500 cursor-pointer hover:text-amber-600"
@click="modelSettings.enable_thinking = null"
x-text="'{{ t('modal.model_settings.enable_thinking_reset') }}'"></p>
</div>
<!-- Thinking Budget -->
<div class="p-4 bg-neutral-50 rounded-xl space-y-3"
x-show="!modelSettings.is_diffusion_model" x-cloak>
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<span class="text-sm font-medium text-neutral-700">{{ t('modal.model_settings.thinking_budget') }}</span>
<p class="text-xs text-neutral-500 mt-0.5">{{ t('modal.model_settings.thinking_budget_hint') }}</p>
</div>
<button type="button" @click="modelSettings.enableThinkingBudget = !modelSettings.enableThinkingBudget; if (!modelSettings.enableThinkingBudget) modelSettings.thinking_budget_tokens = null;"
:class="modelSettings.enableThinkingBudget ? 'bg-black' : 'bg-neutral-200'"
class="relative flex-shrink-0 w-11 h-6 mt-0.5 rounded-full transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black">
<span :class="modelSettings.enableThinkingBudget ? 'translate-x-5' : 'translate-x-0'"
class="block w-5 h-5 bg-white rounded-full shadow-sm transform transition-transform duration-300 absolute top-0.5 left-0.5"></span>
</button>
</div>
<div x-show="modelSettings.enableThinkingBudget" x-transition class="pt-1">
<input type="number" x-model.number="modelSettings.thinking_budget_tokens" placeholder="{{ t('modal.model_settings.thinking_budget_placeholder') }}" min="1" step="100"
class="w-full px-4 py-2.5 border border-neutral-200 rounded-xl text-sm focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all">
</div>
</div>
<!-- Guided Grammar -->
<div class="p-4 bg-neutral-50 rounded-xl space-y-3"
x-show="!modelSettings.is_diffusion_model" x-cloak>
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<span class="text-sm font-medium text-neutral-700">{{ t('modal.model_settings.guided_grammar') }}</span>
<p class="text-xs text-neutral-500 mt-0.5">{{ t('modal.model_settings.guided_grammar_hint') }}</p>
</div>
<button type="button" @click="modelSettings.guided_grammar_enabled = !modelSettings.guided_grammar_enabled; if (!modelSettings.guided_grammar_enabled) modelSettings.guided_grammar = '';"
:class="modelSettings.guided_grammar_enabled ? 'bg-black' : 'bg-neutral-200'"
class="relative flex-shrink-0 w-11 h-6 mt-0.5 rounded-full transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black">
<span :class="modelSettings.guided_grammar_enabled ? 'translate-x-5' : 'translate-x-0'"
class="block w-5 h-5 bg-white rounded-full shadow-sm transform transition-transform duration-300 absolute top-0.5 left-0.5"></span>
</button>
</div>
<div x-show="modelSettings.guided_grammar_enabled" x-transition class="pt-1">
<textarea x-model="modelSettings.guided_grammar"
rows="6"
placeholder="{{ t('modal.model_settings.guided_grammar_placeholder') }}"
class="w-full px-4 py-2.5 border border-neutral-200 rounded-xl text-sm font-mono focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all"></textarea>
</div>
</div>
<!-- Limit Tool Result Tokens -->
<div class="p-4 bg-neutral-50 rounded-xl space-y-3"
x-show="!modelSettings.is_diffusion_model" x-cloak>
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<span class="text-sm font-medium text-neutral-700">{{ t('modal.model_settings.limit_tool_result') }}</span>
<p class="text-xs text-neutral-500 mt-0.5">{{ t('modal.model_settings.limit_tool_result_hint') }}</p>
</div>
<button type="button" @click="modelSettings.enableToolResultLimit = !modelSettings.enableToolResultLimit; if (!modelSettings.enableToolResultLimit) modelSettings.max_tool_result_tokens = null;"
:class="modelSettings.enableToolResultLimit ? 'bg-black' : 'bg-neutral-200'"
class="relative flex-shrink-0 w-11 h-6 mt-0.5 rounded-full transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black">
<span :class="modelSettings.enableToolResultLimit ? 'translate-x-5' : 'translate-x-0'"
class="block w-5 h-5 bg-white rounded-full shadow-sm transform transition-transform duration-300 absolute top-0.5 left-0.5"></span>
</button>
</div>
<div x-show="modelSettings.enableToolResultLimit" x-transition class="pt-1">
<input type="number" x-model.number="modelSettings.max_tool_result_tokens" placeholder="{{ t('modal.model_settings.limit_tool_placeholder') }}" min="100" step="100"
class="w-full px-4 py-2.5 border border-neutral-200 rounded-xl text-sm focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all">
</div>
</div>
<!-- Force Sampling -->
<div class="p-4 bg-neutral-50 rounded-xl"
x-show="!modelSettings.is_diffusion_model" x-cloak>
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<span class="text-sm font-medium text-neutral-700">{{ t('modal.model_settings.force_sampling') }}</span>
<p class="text-xs text-neutral-500 mt-0.5">{{ t('modal.model_settings.force_sampling_hint') }}</p>
</div>
<button type="button" @click="modelSettings.force_sampling = !modelSettings.force_sampling"
:class="modelSettings.force_sampling ? 'bg-black' : 'bg-neutral-200'"
class="relative flex-shrink-0 w-11 h-6 mt-0.5 rounded-full transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black">
<span :class="modelSettings.force_sampling ? 'translate-x-5' : 'translate-x-0'"
class="block w-5 h-5 bg-white rounded-full shadow-sm transform transition-transform duration-300 absolute top-0.5 left-0.5"></span>
</button>
</div>
</div>
<!-- Trust Remote Code (security-sensitive) -->
<div class="p-4 bg-red-50 rounded-xl border border-red-100">
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<span class="text-sm font-medium text-red-700">{{ t('modal.model_settings.trust_remote_code') }}</span>
<p class="text-xs text-red-600 mt-0.5">{{ t('modal.model_settings.trust_remote_code_hint') }}</p>
</div>
<button type="button" @click="modelSettings.trust_remote_code = !modelSettings.trust_remote_code"
:class="modelSettings.trust_remote_code ? 'bg-red-600' : 'bg-neutral-200'"
class="relative flex-shrink-0 w-11 h-6 mt-0.5 rounded-full transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
<span :class="modelSettings.trust_remote_code ? 'translate-x-5' : 'translate-x-0'"
class="block w-5 h-5 bg-white rounded-full shadow-sm transform transition-transform duration-300 absolute top-0.5 left-0.5"></span>
</button>
</div>
</div>
<!-- Chat Template Kwargs -->
<div class="p-4 bg-neutral-50 rounded-xl space-y-3">
<div class="flex items-center justify-between">
<div>
<span class="text-sm font-medium text-neutral-700">{{ t('modal.model_settings.chat_template_kwargs') }}</span>
<p class="text-xs text-neutral-500 mt-0.5">{{ t('modal.model_settings.chat_template_kwargs_hint') }}</p>
</div>
<!-- Add dropdown -->
<div class="relative" x-data="{ addOpen: false }">
<button type="button" @click="addOpen = !addOpen"
class="text-xs text-neutral-500 hover:text-neutral-700 font-medium transition-colors">{{ t('modal.model_settings.add_kwarg') }}</button>
<div x-show="addOpen" @click.away="addOpen = false" x-transition
class="absolute right-0 mt-1 w-44 bg-white border border-neutral-200 rounded-lg shadow-lg z-10 py-1">
<button type="button"
x-show="!modelSettings.is_diffusion_model && !modelSettings.ctKwargEntries.some(e => e.type === 'enable_thinking')"
@click="modelSettings.ctKwargEntries.push({type: 'enable_thinking', value: 'true', force: false}); addOpen = false"
class="w-full text-left px-3 py-1.5 text-sm text-neutral-700 hover:bg-neutral-50 transition-colors">{{ t('modal.model_settings.kwarg_enable_thinking') }}</button>
<button type="button"
x-show="!modelSettings.is_diffusion_model && !modelSettings.ctKwargEntries.some(e => e.type === 'reasoning_effort')"
@click="modelSettings.ctKwargEntries.push({type: 'reasoning_effort', value: 'low', force: false}); addOpen = false"
class="w-full text-left px-3 py-1.5 text-sm text-neutral-700 hover:bg-neutral-50 transition-colors">{{ t('modal.model_settings.kwarg_reasoning_effort') }}</button>
<button type="button"
@click="modelSettings.ctKwargEntries.push({type: 'custom', key: '', value: '', force: false}); addOpen = false"
class="w-full text-left px-3 py-1.5 text-sm text-neutral-700 hover:bg-neutral-50 transition-colors">{{ t('modal.model_settings.kwarg_custom') }}</button>
</div>
</div>
</div>
<!-- Dynamic entries -->
<template x-for="(entry, idx) in modelSettings.ctKwargEntries" :key="idx">
<div class="space-y-2">
<!-- Row 1: Label + Remove -->
<div class="flex items-center justify-between">
<template x-if="entry.type === 'enable_thinking'">
<span class="text-xs font-bold uppercase tracking-wider text-neutral-500">{{ t('modal.model_settings.kwarg_enable_thinking') }}</span>
</template>
<template x-if="entry.type === 'reasoning_effort'">
<span class="text-xs font-bold uppercase tracking-wider text-neutral-500">{{ t('modal.model_settings.kwarg_reasoning_effort') }}</span>
</template>
<template x-if="entry.type === 'custom'">
<span class="text-xs font-bold uppercase tracking-wider text-neutral-500">{{ t('modal.model_settings.kwarg_custom') }}</span>
</template>
<button type="button" @click="modelSettings.ctKwargEntries.splice(idx, 1)"
class="p-1 text-neutral-400 hover:text-red-500 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</button>
</div>
<!-- Row 2: Value + Force -->
<div class="flex gap-2 items-center">
<template x-if="entry.type === 'enable_thinking'">
<select x-model="entry.value"
class="flex-1 px-3 py-1.5 border border-neutral-200 rounded-lg text-sm focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all bg-white">
<option value="true">true</option>
<option value="false">false</option>
</select>
</template>
<template x-if="entry.type === 'reasoning_effort'">
<select x-model="entry.value"
class="flex-1 px-3 py-1.5 border border-neutral-200 rounded-lg text-sm focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all bg-white">
<option value="low">{{ t('modal.model_settings.kwarg_effort_low') }}</option>
<option value="medium">{{ t('modal.model_settings.kwarg_effort_medium') }}</option>
<option value="high">{{ t('modal.model_settings.kwarg_effort_high') }}</option>
</select>
</template>
<template x-if="entry.type !== 'custom'">
<label class="flex items-center gap-1 flex-shrink-0" title="{{ t('modal.model_settings.kwarg_force_tooltip') }}">
<input type="checkbox" x-model="entry.force"
class="w-3.5 h-3.5 rounded border-neutral-300 text-neutral-900 focus:ring-neutral-900">
<span class="text-xs text-neutral-400">{{ t('modal.model_settings.kwarg_force') }}</span>
</label>
</template>
</div>
<!-- Row 2b: Custom key/value + Force (stacked) -->
<template x-if="entry.type === 'custom'">
<div class="space-y-2">
<input type="text" x-model="entry.key" placeholder="{{ t('modal.model_settings.kwarg_custom_key_placeholder') }}"
class="w-full px-3 py-1.5 border border-neutral-200 rounded-lg text-sm focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all">
<div class="flex gap-2 items-center">
<input type="text" x-model="entry.value" placeholder="{{ t('modal.model_settings.kwarg_custom_value_placeholder') }}"
class="flex-1 px-3 py-1.5 border border-neutral-200 rounded-lg text-sm focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all min-w-0">
<label class="flex items-center gap-1 flex-shrink-0" title="{{ t('modal.model_settings.kwarg_force_tooltip') }}">
<input type="checkbox" x-model="entry.force"
class="w-3.5 h-3.5 rounded border-neutral-300 text-neutral-900 focus:ring-neutral-900">
<span class="text-xs text-neutral-400">{{ t('modal.model_settings.kwarg_force') }}</span>
</label>
</div>
</div>
</template>
</div>
</template>
<!-- Empty state -->
<p x-show="modelSettings.ctKwargEntries.length === 0" class="text-xs text-neutral-400 text-center py-1">{{ t('modal.model_settings.no_kwargs') }}</p>
</div>
<div x-show="!modelSettings.is_diffusion_model" x-cloak>
<!-- Acceleration -->
<h4 class="text-xs font-bold uppercase tracking-widest text-neutral-400 mb-3">{{ t('modal.model_settings.acceleration_label') }}</h4>
<!-- Lightning MTP (built-in MTP head speculative decoding) -->
<div class="p-4 bg-neutral-50 rounded-xl space-y-3 mb-6">
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<span class="text-sm font-medium text-neutral-700">{{ t('modal.model_settings.lightning_mtp') }}</span>
<p class="text-xs text-neutral-500 mt-0.5">{{ t('modal.model_settings.lightning_mtp_hint') }}</p>
<p x-show="!modelSettings.mtp_compatible && modelSettings.mtp_compatibility_reason"
class="text-xs text-amber-600 mt-1"
x-text="modelSettings.mtp_compatibility_reason"></p>
<p x-show="modelSettings.mtp_compatible && modelSettings.dflash_enabled"
class="text-xs text-amber-600 mt-1">{{ t('modal.model_settings.mtp_conflict_dflash') }}</p>
</div>
<button type="button"
@click="if ((modelSettings.mtp_compatible || modelSettings.mtp_enabled) && !modelSettings.dflash_enabled && !modelSettings.vlm_mtp_enabled) modelSettings.mtp_enabled = !modelSettings.mtp_enabled"
:disabled="(!modelSettings.mtp_compatible && !modelSettings.mtp_enabled) || modelSettings.dflash_enabled || modelSettings.vlm_mtp_enabled"
:title="(!modelSettings.mtp_compatible && !modelSettings.mtp_enabled)
? (modelSettings.mtp_compatibility_reason || 'Unsupported model')
: (modelSettings.dflash_enabled
? '{{ t('modal.model_settings.mtp_conflict_dflash') }}'
: (modelSettings.vlm_mtp_enabled
? '{{ t('modal.model_settings.vlm_mtp_conflict') }}'
: ''))"
:class="[
modelSettings.mtp_enabled ? 'bg-black' : 'bg-neutral-200',
((!modelSettings.mtp_compatible && !modelSettings.mtp_enabled) || modelSettings.dflash_enabled || modelSettings.vlm_mtp_enabled) ? 'opacity-40 cursor-not-allowed' : ''
]"
class="relative flex-shrink-0 w-11 h-6 mt-0.5 rounded-full transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black">
<span :class="modelSettings.mtp_enabled ? 'translate-x-5' : 'translate-x-0'"
class="block w-5 h-5 bg-white rounded-full shadow-sm transform transition-transform duration-300 absolute top-0.5 left-0.5"></span>
</button>
</div>
</div>
<!-- Experimental Features -->
<h4 class="text-xs font-bold uppercase tracking-widest text-neutral-400 mb-3">{{ t('modal.model_settings.experimental_label') }}</h4>
<!-- TurboQuant KV Cache -->
<div class="p-4 bg-neutral-50 rounded-xl space-y-3 mb-3">
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<span class="text-sm font-medium text-neutral-700">{{ t('modal.model_settings.turboquant_kv') }}</span>
<p class="text-xs text-neutral-500 mt-0.5">{{ t('modal.model_settings.turboquant_kv_hint') }}</p>
</div>
<button type="button"
@click="if (!modelSettings.is_paroquant && !modelSettings.vlm_mtp_enabled) modelSettings.turboquant_kv_enabled = !modelSettings.turboquant_kv_enabled"
:disabled="modelSettings.is_paroquant || modelSettings.vlm_mtp_enabled"
:title="modelSettings.is_paroquant ? modelSettings.paroquant_reason : (modelSettings.vlm_mtp_enabled ? '{{ t('modal.model_settings.vlm_mtp_conflict') }}' : '')"
:class="[
modelSettings.turboquant_kv_enabled ? 'bg-black' : 'bg-neutral-200',
(modelSettings.is_paroquant || modelSettings.vlm_mtp_enabled) ? 'opacity-40 cursor-not-allowed' : ''
]"
class="relative flex-shrink-0 w-11 h-6 mt-0.5 rounded-full transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black">
<span :class="modelSettings.turboquant_kv_enabled ? 'translate-x-5' : 'translate-x-0'"
class="block w-5 h-5 bg-white rounded-full shadow-sm transform transition-transform duration-300 absolute top-0.5 left-0.5"></span>
</button>
</div>
<div x-show="modelSettings.turboquant_kv_enabled" x-transition class="pt-1">
<label class="block text-xs font-bold uppercase tracking-wider text-neutral-500 mb-2">{{ t('modal.model_settings.turboquant_kv_bits_label') }}</label>
<select x-model="modelSettings.turboquant_kv_bits"
class="w-full px-4 py-2.5 border border-neutral-200 rounded-xl text-sm focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all bg-white">
<option value="2">2-bit</option>
<option value="2.5">2.5-bit</option>
<option value="3">3-bit</option>
<option value="3.5">3.5-bit</option>
<option value="4">4-bit</option>
<option value="6">6-bit</option>
<option value="8">8-bit</option>
</select>
</div>
</div>
<!-- IndexCache (DSA models only) -->
<template x-if="DSA_MODEL_TYPES.has(selectedModel?.config_model_type || '')">
<div class="p-4 bg-neutral-50 rounded-xl space-y-3 mb-3">
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<span class="text-sm font-medium text-neutral-700">{{ t('modal.model_settings.index_cache') }}</span>
<p class="text-xs text-neutral-500 mt-0.5">{{ t('modal.model_settings.index_cache_hint') }} (<a href="https://github.com/THUDM/IndexCache" target="_blank" rel="noopener" class="text-blue-500 hover:text-blue-700 underline">GitHub</a>)</p>
</div>
<button type="button"
@click="if (!modelSettings.is_paroquant) { modelSettings.enableIndexCache = !modelSettings.enableIndexCache; if (!modelSettings.enableIndexCache) modelSettings.index_cache_freq = null; }"
:disabled="modelSettings.is_paroquant"
:title="modelSettings.is_paroquant ? modelSettings.paroquant_reason : ''"
:class="[
modelSettings.enableIndexCache ? 'bg-black' : 'bg-neutral-200',
modelSettings.is_paroquant ? 'opacity-40 cursor-not-allowed' : ''
]"
class="relative flex-shrink-0 w-11 h-6 mt-0.5 rounded-full transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black">
<span :class="modelSettings.enableIndexCache ? 'translate-x-5' : 'translate-x-0'"
class="block w-5 h-5 bg-white rounded-full shadow-sm transform transition-transform duration-300 absolute top-0.5 left-0.5"></span>
</button>
</div>
<div x-show="modelSettings.enableIndexCache" x-transition class="pt-1">
<label class="block text-xs font-bold uppercase tracking-wider text-neutral-500 mb-2">{{ t('modal.model_settings.index_cache_freq') }}</label>
<input type="number" x-model.number="modelSettings.index_cache_freq" min="2" max="16" step="1" placeholder="4"
class="w-full px-4 py-2.5 border border-neutral-200 rounded-xl text-sm focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all">
</div>
</div>
</template>
<!-- SpecPrefill -->
<div class="p-4 bg-neutral-50 rounded-xl space-y-3">
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<span class="text-sm font-medium text-neutral-700">SpecPrefill</span>
<p class="text-xs text-neutral-500 mt-0.5">Attention-based sparse prefill for MoE/hybrid models. (<a href="https://arxiv.org/abs/2502.02789" target="_blank" rel="noopener" class="text-blue-500 hover:text-blue-700 underline">Paper</a>) (<a href="https://huggingface.co/Thump604/specprefill-paper" target="_blank" rel="noopener" class="text-blue-500 hover:text-blue-700 underline">HuggingFace</a>)</p>
</div>
<button type="button"
@click="if (!modelSettings.is_paroquant && !modelSettings.vlm_mtp_enabled) modelSettings.specprefill_enabled = !modelSettings.specprefill_enabled"
:disabled="modelSettings.is_paroquant || modelSettings.vlm_mtp_enabled"
:title="modelSettings.is_paroquant ? modelSettings.paroquant_reason : (modelSettings.vlm_mtp_enabled ? '{{ t('modal.model_settings.vlm_mtp_conflict') }}' : '')"
:class="[
modelSettings.specprefill_enabled ? 'bg-black' : 'bg-neutral-200',
(modelSettings.is_paroquant || modelSettings.vlm_mtp_enabled) ? 'opacity-40 cursor-not-allowed' : ''
]"
class="relative flex-shrink-0 w-11 h-6 mt-0.5 rounded-full transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black">
<span :class="modelSettings.specprefill_enabled ? 'translate-x-5' : 'translate-x-0'"
class="block w-5 h-5 bg-white rounded-full shadow-sm transform transition-transform duration-300 absolute top-0.5 left-0.5"></span>
</button>
</div>
<div x-show="modelSettings.specprefill_enabled" x-transition class="space-y-3 pt-1">
<div>
<label class="block text-xs font-bold uppercase tracking-wider text-neutral-500 mb-2">Draft Model</label>
<select x-model="modelSettings.specprefill_draft_model"
class="w-full px-4 py-2.5 border border-neutral-200 rounded-xl text-sm focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all bg-white">
<option value="">Select draft model...</option>
<template x-for="m in specPrefillDraftModelCandidates()" :key="m.id">
<option :value="m.model_path || m.id" x-text="m.id" :selected="modelSettings.specprefill_draft_model === (m.model_path || m.id)"></option>
</template>
</select>
<p class="text-xs text-neutral-400 mt-1">Small model sharing tokenizer with target (e.g. Qwen3.5-0.8B for 35B)</p>
</div>
<div>
<label class="block text-xs font-bold uppercase tracking-wider text-neutral-500 mb-2">Keep Rate</label>
<select x-model="modelSettings.specprefill_keep_pct"
class="w-full px-4 py-2.5 border border-neutral-200 rounded-xl text-sm focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all bg-white">
<option value="0.1">10% — Aggressive (~5-7x, some quality loss)</option>
<option value="0.2">20% — Balanced (~3x, recommended)</option>
<option value="0.25">25% — Conservative+ (~2.5x)</option>
<option value="0.3">30% — Conservative (~2.2x)</option>
<option value="0.4">40% — Mild (~1.8x)</option>
<option value="0.5">50% — Minimal (~1.5x)</option>
</select>
</div>
<div>
<label class="block text-xs font-bold uppercase tracking-wider text-neutral-500 mb-2">Threshold (tokens)</label>
<input type="number" x-model.number="modelSettings.specprefill_threshold" min="1024" max="131072" step="1024" placeholder="8192"
class="w-full px-4 py-2.5 border border-neutral-200 rounded-xl text-sm focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all">
<p class="text-xs text-neutral-400 mt-1">Min tokens to trigger (shorter prompts use full prefill)</p>
</div>
</div>
</div>
<!-- DFlash -->
<div class="p-4 bg-neutral-50 rounded-xl space-y-3">
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<span class="text-sm font-medium text-neutral-700">DFlash</span>
<p class="text-xs text-neutral-500 mt-0.5">Block diffusion speculative decoding for 3-4x faster generation. Supports Qwen (3, 3.5, 3.6) and Gemma4 model families. Requires a DFlash draft model checkpoint.<br><strong>Single-stream only: requests run one at a time.</strong><br>* MLX impl by bstnxbt(<a href="https://github.com/bstnxbt/dflash-mlx" target="_blank" rel="noopener" class="text-blue-500 hover:text-blue-700 underline">GitHub</a>)</p>
<p x-show="!modelSettings.dflash_compatible && modelSettings.dflash_compatibility_reason"
class="text-xs text-amber-600 mt-1"
x-text="modelSettings.dflash_compatibility_reason"></p>
</div>
<button type="button"
@click="if (modelSettings.dflash_compatible && !modelSettings.mtp_enabled && !modelSettings.vlm_mtp_enabled) modelSettings.dflash_enabled = !modelSettings.dflash_enabled"
:disabled="!modelSettings.dflash_compatible || modelSettings.mtp_enabled || modelSettings.vlm_mtp_enabled"
:title="modelSettings.dflash_compatible
? (modelSettings.mtp_enabled
? '{{ t('modal.model_settings.mtp_conflict_dflash') }}'
: (modelSettings.vlm_mtp_enabled
? '{{ t('modal.model_settings.vlm_mtp_conflict') }}'
: ''))
: (modelSettings.dflash_compatibility_reason || 'Unsupported model')"
:class="[
modelSettings.dflash_enabled ? 'bg-black' : 'bg-neutral-200',
(!modelSettings.dflash_compatible || modelSettings.mtp_enabled || modelSettings.vlm_mtp_enabled) ? 'opacity-40 cursor-not-allowed' : ''
]"
class="relative flex-shrink-0 w-11 h-6 mt-0.5 rounded-full transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black">
<span :class="modelSettings.dflash_enabled ? 'translate-x-5' : 'translate-x-0'"
class="block w-5 h-5 bg-white rounded-full shadow-sm transform transition-transform duration-300 absolute top-0.5 left-0.5"></span>
</button>
</div>
<div x-show="modelSettings.dflash_enabled" x-transition class="space-y-3 pt-1">
<div>
<label class="block text-xs font-bold uppercase tracking-wider text-neutral-500 mb-2">Draft Model</label>
<select x-model="modelSettings.dflash_draft_model"
class="w-full px-4 py-2.5 border border-neutral-200 rounded-xl text-sm focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all bg-white">
<option value="">Select draft model...</option>
<template x-for="m in dflashDraftModelCandidates()" :key="m.id">
<option :value="m.model_path || m.id" x-text="m.id" :selected="modelSettings.dflash_draft_model === (m.model_path || m.id)"></option>
</template>
</select>
<p class="text-xs text-neutral-400 mt-1">DFlash draft checkpoint (e.g. z-lab/Qwen3-4B-DFlash-b16, z-lab/gemma-4-26B-A4B-it-DFlash). Note: -DFlash suffix only; -assistant variants are for MTP.</p>
</div>
<div class="bg-neutral-50 rounded-xl">
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<span class="text-sm font-medium text-neutral-700">Quantization</span>
<p class="text-xs text-neutral-500 mt-0.5">Enable quantization for the draft model (weight, activation bits &amp; group size).</p>
</div>
<button type="button" @click="modelSettings.dflash_draft_quant_enabled = !modelSettings.dflash_draft_quant_enabled"
:class="modelSettings.dflash_draft_quant_enabled ? 'bg-black' : 'bg-neutral-200'"
class="relative flex-shrink-0 w-11 h-6 mt-0.5 rounded-full transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black">
<span :class="modelSettings.dflash_draft_quant_enabled ? 'translate-x-5' : 'translate-x-0'"
class="block w-5 h-5 bg-white rounded-full shadow-sm transform transition-transform duration-300 absolute top-0.5 left-0.5"></span>
</button>
</div>
</div>
<div class="grid grid-cols-3 gap-3 pl-4" x-show="modelSettings.dflash_draft_quant_enabled" x-transition>
<div>
<label class="block text-xs font-bold uppercase tracking-wider text-neutral-500 mb-2">Weight Bits</label>
<select x-model="modelSettings.dflash_draft_quant_weight_bits"
class="w-full px-4 py-2.5 border border-neutral-200 rounded-xl text-sm focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all bg-white">
<option value="2">2-bit</option>
<option value="4" selected>4-bit</option>
<option value="8">8-bit</option>
</select>
</div>
<div>
<label class="block text-xs font-bold uppercase tracking-wider text-neutral-500 mb-2">Activation Bits</label>
<select x-model="modelSettings.dflash_draft_quant_activation_bits"
class="w-full px-4 py-2.5 border border-neutral-200 rounded-xl text-sm focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all bg-white">
<option value="16" selected>16-bit</option>
<option value="32">32-bit</option>
</select>
</div>
<div>
<label class="block text-xs font-bold uppercase tracking-wider text-neutral-500 mb-2">Group Size</label>
<select x-model="modelSettings.dflash_draft_quant_group_size"
class="w-full px-4 py-2.5 border border-neutral-200 rounded-xl text-sm focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all bg-white">
<option value="32">32</option>
<option value="64" selected>64</option>
<option value="128">128</option>
</select>
</div>
</div>
<div>
<label class="block text-xs font-bold uppercase tracking-wider text-neutral-500 mb-2">Max Context (fallback threshold)</label>
<input type="number" x-model.number="modelSettings.dflash_max_ctx" min="1" step="1024" placeholder="unlimited"
class="w-full px-4 py-2.5 border border-neutral-200 rounded-xl text-sm focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all">
<p class="text-xs text-neutral-400 mt-1">Prompts at or above this token count switch to BatchedEngine. Leave empty for unlimited.</p>
</div>
<div class="pt-2 border-t border-neutral-200 space-y-3">
<p class="text-xs font-bold uppercase tracking-wider text-neutral-500">Long-context tuning</p>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs font-bold uppercase tracking-wider text-neutral-500 mb-2">Draft window size</label>
<input type="number" x-model.number="modelSettings.dflash_draft_window_size" min="1" step="128" placeholder="1024"
class="w-full px-4 py-2.5 border border-neutral-200 rounded-xl text-sm focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all">
<p class="text-xs text-neutral-400 mt-1">Draft model sliding-attention window. Helps stabilise acceptance on long contexts. Leave empty for dflash default (1024).</p>
</div>
<div>
<label class="block text-xs font-bold uppercase tracking-wider text-neutral-500 mb-2">Draft sink size</label>
<input type="number" x-model.number="modelSettings.dflash_draft_sink_size" min="0" step="8" placeholder="64"
class="w-full px-4 py-2.5 border border-neutral-200 rounded-xl text-sm focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all">
<p class="text-xs text-neutral-400 mt-1">Attention-sink tokens always kept regardless of window. Leave empty for dflash default (64).</p>
</div>
</div>
<div>
<label class="block text-xs font-bold uppercase tracking-wider text-neutral-500 mb-2">Verify mode</label>
<select x-model="modelSettings.dflash_verify_mode"
class="w-full px-4 py-2.5 border border-neutral-200 rounded-xl text-sm focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all bg-white">
<option value="adaptive">adaptive (default)</option>
<option value="dflash">dflash</option>
<option value="ddtree">ddtree</option>
<option value="off">off</option>
</select>
<p class="text-xs text-neutral-400 mt-1">Verifier algorithm. "adaptive" shrinks block size when acceptance drops; "off" disables speculative verify.</p>
</div>
</div>
<div class="flex items-start justify-between gap-3 pt-2 border-t border-neutral-200">
<div class="min-w-0">
<span class="text-sm font-medium text-neutral-700">In-memory cache</span>
<p class="text-xs text-neutral-500 mt-0.5">DFlash L1 prefix snapshot cache in RAM. Speeds up multi-turn chats with shared prefixes.</p>
</div>
<button type="button" @click="modelSettings.dflash_in_memory_cache = !modelSettings.dflash_in_memory_cache; if (!modelSettings.dflash_in_memory_cache) modelSettings.dflash_ssd_cache = false"
:class="modelSettings.dflash_in_memory_cache ? 'bg-black' : 'bg-neutral-200'"
class="relative flex-shrink-0 w-11 h-6 mt-0.5 rounded-full transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black">
<span :class="modelSettings.dflash_in_memory_cache ? 'translate-x-5' : 'translate-x-0'"
class="block w-5 h-5 bg-white rounded-full shadow-sm transform transition-transform duration-300 absolute top-0.5 left-0.5"></span>
</button>
</div>
<div x-show="modelSettings.dflash_in_memory_cache" x-transition>
<label class="block text-xs font-bold uppercase tracking-wider text-neutral-500 mb-2">In-memory cache max entries</label>
<input type="number" x-model.number="modelSettings.dflash_in_memory_cache_max_entries" min="1" max="128" step="1" placeholder="4"
class="w-full px-4 py-2.5 border border-neutral-200 rounded-xl text-sm focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all">
<p class="text-xs text-neutral-400 mt-1">Maximum number of prefix snapshots kept in L1 cache. Each entry stores KV + draft GDN state for one conversation prefix.</p>
</div>
<div x-show="modelSettings.dflash_in_memory_cache" x-transition>
<label class="block text-xs font-bold uppercase tracking-wider text-neutral-500 mb-2">In-memory cache size (GiB)</label>
<input type="number" x-model.number="modelSettings.dflash_in_memory_cache_max_gib" min="1" max="256" step="1" placeholder="8"
class="w-full px-4 py-2.5 border border-neutral-200 rounded-xl text-sm focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all">
<p class="text-xs text-neutral-400 mt-1">Byte budget for L1 snapshots; LRU evicts when exceeded.</p>
</div>
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<span class="text-sm font-medium text-neutral-700">SSD cache</span>
<p class="text-xs text-neutral-500 mt-0.5">L2 spill of evicted L1 entries to disk. Uses the oMLX paged SSD cache directory (<code>dflash_l2/</code>).</p>
<p x-show="!modelSettings.dflash_ssd_cache_available" class="text-xs text-amber-600 mt-1">Enable oMLX paged SSD cache first (<code>--paged-ssd-cache-dir</code>).</p>
<p x-show="modelSettings.dflash_ssd_cache_available && !modelSettings.dflash_in_memory_cache" class="text-xs text-amber-600 mt-1">Requires in-memory cache to be enabled.</p>
</div>
<button type="button"
@click="if (modelSettings.dflash_ssd_cache_available && modelSettings.dflash_in_memory_cache) modelSettings.dflash_ssd_cache = !modelSettings.dflash_ssd_cache"
:disabled="!modelSettings.dflash_ssd_cache_available || !modelSettings.dflash_in_memory_cache"
:class="[
modelSettings.dflash_ssd_cache ? 'bg-black' : 'bg-neutral-200',
(!modelSettings.dflash_ssd_cache_available || !modelSettings.dflash_in_memory_cache) ? 'opacity-40 cursor-not-allowed' : ''
]"
class="relative flex-shrink-0 w-11 h-6 mt-0.5 rounded-full transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black">
<span :class="modelSettings.dflash_ssd_cache ? 'translate-x-5' : 'translate-x-0'"
class="block w-5 h-5 bg-white rounded-full shadow-sm transform transition-transform duration-300 absolute top-0.5 left-0.5"></span>
</button>
</div>
<div x-show="modelSettings.dflash_ssd_cache" x-transition>
<label class="block text-xs font-bold uppercase tracking-wider text-neutral-500 mb-2">SSD cache size (GiB)</label>
<input type="number" x-model.number="modelSettings.dflash_ssd_cache_max_gib" min="1" max="1024" step="1" placeholder="20"
class="w-full px-4 py-2.5 border border-neutral-200 rounded-xl text-sm focus:ring-2 focus:ring-neutral-900 focus:border-transparent transition-all">
<p class="text-xs text-neutral-400 mt-1">Disk budget for L2 spill; oldest entries are evicted when exceeded.</p>
</div>
</div>
</div>
<!-- VLM MTP (mlx-vlm 191d7c8+, gemma4_assistant drafter) -->
<div class="p-4 bg-neutral-50 rounded-xl space-y-3 mt-3">
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<span class="text-sm font-medium text-neutral-700">{{ t('modal.model_settings.vlm_mtp') }}</span>
<p class="text-xs text-neutral-500 mt-0.5">{{ t('modal.model_settings.vlm_mtp_hint') | safe }}</p>
<p x-show="modelSettings.dflash_enabled || modelSettings.specprefill_enabled || modelSettings.mtp_enabled || modelSettings.turboquant_kv_enabled"
class="text-xs text-amber-600 mt-1">{{ t('modal.model_settings.vlm_mtp_conflict') }}</p>
</div>
<button type="button"
@click="if (!modelSettings.is_paroquant && !modelSettings.dflash_enabled && !modelSettings.specprefill_enabled && !modelSettings.mtp_enabled && !modelSettings.turboquant_kv_enabled) modelSettings.vlm_mtp_enabled = !modelSettings.vlm_mtp_enabled"
:disabled="modelSettings.is_paroquant || modelSettings.dflash_enabled || modelSettings.specprefill_enabled || modelSettings.mtp_enabled || modelSettings.turboquant_kv_enabled"
:title="modelSettings.is_paroquant ? modelSettings.paroquant_reason : ''"
:class="[
modelSettings.vlm_mtp_enabled ? 'bg-black' : 'bg-neutral-200',
(modelSettings.is_paroquant || modelSettings.dflash_enabled || modelSettings.specprefill_enabled || modelSettings.mtp_enabled || modelSettings.turboquant_kv_enabled) ? 'opacity-40 cursor-not-allowed' : ''
]"
class="relative flex-shrink-0 w-11 h-6 mt-0.5 rounded-full transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black">
<span :class="modelSettings.vlm_mtp_enabled ? 'translate-x-5' : 'translate-x-0'"
class="block w-5 h-5 bg-white rounded-full shadow-sm transform transition-transform duration-300 absolute top-0.5 left-0.5"></span>
</button>
</div>
<div x-show="modelSettings.vlm_mtp_enabled" class="space-y-2">
<label class="block text-xs font-medium text-neutral-600">{{ t('modal.model_settings.vlm_mtp_draft_model') }}</label>
<select x-model="modelSettings.vlm_mtp_draft_model"
class="w-full px-3 py-2 text-sm border border-neutral-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-black bg-white">
<option value="">{{ t('modal.model_settings.vlm_mtp_draft_model_placeholder') }}</option>
<template x-for="m in vlmMtpDraftModelCandidates()" :key="m.id">
<option :value="m.id" x-text="m.id"></option>
</template>
</select>
<label class="block text-xs font-medium text-neutral-600 mt-2">{{ t('modal.model_settings.vlm_mtp_block_size') }}</label>
<input type="number"
min="1"
max="16"
x-model.number="modelSettings.vlm_mtp_draft_block_size"
placeholder="4"
class="w-full px-3 py-2 text-sm border border-neutral-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-black" />
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Actions -->
<div class="flex items-center justify-end gap-3 mt-8">
<button @click="showModelSettingsModal = false"
class="px-5 py-2.5 text-sm font-medium text-neutral-600 hover:text-neutral-900 hover:bg-neutral-100 rounded-full transition-all">
{{ t('modal.model_settings.cancel') }}
</button>
<button @click="saveModelSettings"
:disabled="savingModelSettings"
class="px-6 py-2.5 bg-neutral-900 text-white text-sm font-semibold rounded-full
hover:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:ring-offset-2
transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed">
<span x-show="!savingModelSettings">{{ t('modal.model_settings.save') }}</span>
<span x-show="savingModelSettings">{{ t('modal.model_settings.saving') }}</span>
</button>
</div>
</div>
</div>
</div>