// OCR model types that require temperature=0.0 (deterministic output) const OCR_CONFIG_MODEL_TYPES = new Set([ 'deepseekocr', 'deepseekocr_2', 'dots_ocr', 'glm_ocr', ]); const DSA_MODEL_TYPES = new Set([ 'deepseek_v32', 'glm_moe_dsa', ]); const DIFFUSION_CONFIG_MODEL_TYPES = new Set([ 'diffusion_gemma', ]); const DIFFUSION_UNSUPPORTED_PROFILE_FIELDS = new Set([ 'top_p', 'top_k', 'min_p', 'repetition_penalty', 'presence_penalty', 'force_sampling', 'enable_thinking', 'preserve_thinking', 'thinking_budget_enabled', 'thinking_budget_tokens', 'reasoning_parser', 'guided_grammar_enabled', 'guided_grammar', 'max_tool_result_tokens', 'index_cache_freq', 'turboquant_kv_enabled', 'turboquant_kv_bits', 'turboquant_skip_last', 'specprefill_enabled', 'specprefill_draft_model', 'specprefill_keep_pct', 'specprefill_threshold', 'dflash_enabled', 'dflash_draft_model', 'dflash_draft_quant_enabled', 'dflash_draft_quant_weight_bits', 'dflash_draft_quant_activation_bits', 'dflash_draft_quant_group_size', 'dflash_max_ctx', 'dflash_in_memory_cache', 'dflash_in_memory_cache_max_entries', 'dflash_in_memory_cache_max_bytes', 'dflash_ssd_cache', 'dflash_ssd_cache_max_bytes', 'dflash_draft_window_size', 'dflash_draft_sink_size', 'dflash_verify_mode', 'mtp_enabled', 'vlm_mtp_enabled', 'vlm_mtp_draft_model', 'vlm_mtp_draft_block_size', ]); const DIFFUSION_UNSUPPORTED_CT_KWARGS = new Set([ 'enable_thinking', 'reasoning_effort', 'preserve_thinking', ]); const VLM_MTP_DRAFTER_CONFIG_MODEL_TYPES = new Set([ 'gemma4_assistant', 'gemma4_unified_assistant', 'qwen3_5_mtp', ]); const DASHBOARD_MAIN_TABS = new Set(['status', 'settings', 'models', 'logs', 'bench']); const DASHBOARD_SETTINGS_TABS = new Set(['global', 'integrations', 'models']); const DASHBOARD_MODELS_TABS = new Set(['manager', 'downloader', 'quantizer', 'uploader']); const DASHBOARD_BENCH_TABS = new Set(['throughput', 'accuracy']); // Default sort for the settings and manager model tables. Also the target // state for the "reset sort" action. const MODELS_SORT_DEFAULT = { by: 'id', order: 'asc' }; const MANAGER_SORT_DEFAULT = { by: 'name', order: 'asc' }; function dashboard() { return { // Theme theme: localStorage.getItem('omlx-chat-theme') || 'auto', activeTheme: 'light', // Will be updated by applyTheme systemThemeListener: null, // Mobile menu mobileMenuOpen: false, // Main tab state (Status, Settings, or Logs) mainTab: 'status', activeTab: 'global', settingsDropdown: false, themeDropdown: false, // Global settings globalSettings: { base_path: '', server: { host: '127.0.0.1', port: 8000, log_level: 'info', sse_keepalive_mode: 'chunk', burst_decode_mode: 'balanced', preserve_mid_system_cache: true }, model: { model_dirs: [''], model_fallback: false, hide_helper_models: false }, memory: { prefill_memory_guard: true, memory_guard_tier: 'balanced', memory_guard_custom_ceiling_gb: 0 }, scheduler: { max_concurrent_requests: 8, embedding_batch_size: 32, chunked_prefill: false }, cache: { enabled: true, ssd_cache_dir: '', ssd_cache_max_size: 'auto', hot_cache_max_size: '0', initial_cache_blocks: 256, hot_cache_only: false }, sampling: { max_context_window: 32768, max_context_window_policy: null, max_tokens: 32768, temperature: 1.0, top_p: 0.95, top_k: 0, repetition_penalty: 1.0 }, mcp: { config_path: '' }, huggingface: { endpoint: '', hf_cache_enabled: true, hf_cache_path: '' }, network: { http_proxy: '', https_proxy: '', no_proxy: '', ca_bundle: '' }, auth: { api_key_set: false, api_key: '', skip_api_key_verification: false, sub_keys: [] }, claude_code: { context_scaling_enabled: false, target_context_size: 200000, mode: 'cloud', opus_model: null, sonnet_model: null, haiku_model: null }, integrations: { copilot_model: null, codex_model: null, opencode_model: null, openclaw_model: null, hermes_model: null, pi_model: null, openclaw_tools_profile: 'full', markitdown_enabled: true, markitdown_expose_model: false, markitdown_max_file_size_mb: 25, markitdown_max_files_per_request: 5, markitdown_pdf_processing_engine: 'markitdown', }, ui: { language: 'en' }, idle_timeout: { idle_timeout_seconds: null }, system: { total_memory_bytes: 0, total_memory: '', auto_model_memory: '', ssd_total_bytes: 0, ssd_total: '' }, }, // Cache slider (0-100%) cachePercent: 10, editingCache: false, // Hot cache slider (0-50%) hotCachePercent: 0, // Editing state for direct GB input editingHotCache: false, // Idle timeout string value for select binding (null ↔ '') idleTimeoutValue: '', // Models models: [], loadingModels: false, reloading: false, // Sort state persists across refreshes/restarts via localStorage. sortBy: localStorage.getItem('omlx_models_sort_by') || MODELS_SORT_DEFAULT.by, sortOrder: localStorage.getItem('omlx_models_sort_order') || MODELS_SORT_DEFAULT.order, modelSearch: '', // Manager tab (Browse Models > Local) sort + search state. managerSortBy: localStorage.getItem('omlx_manager_sort_by') || MANAGER_SORT_DEFAULT.by, managerSortOrder: localStorage.getItem('omlx_manager_sort_order') || MANAGER_SORT_DEFAULT.order, managerSearch: '', // Auth UI state showApiKey: false, // Sub key management newSubKeyValue: '', newSubKeyName: '', showNewSubKeyForm: false, showNewSubKey: false, subKeyError: '', showSubKeys: {}, // Saving state saving: false, saveSuccess: false, saveMessage: '', saveError: '', // Model settings modal showModelSettingsModal: false, selectedModel: null, modelSettings: { model_alias: '', model_type_override: '', max_context_window: null, max_tokens: null, temperature: null, top_p: null, top_k: null, repetition_penalty: null, min_p: null, presence_penalty: null, force_sampling: false, enableToolResultLimit: false, max_tool_result_tokens: null, ctKwargEntries: [], is_diffusion_model: false, trust_remote_code: false, }, savingModelSettings: false, loadingGenDefaults: false, reasoningParsers: [], // Profile / template / preset state profiles: [], // per-model profiles for selectedModel templates: [], // global templates presets: [], // curated presets (bundled + remote refresh) profileFields: { universal: [], model_specific: [] }, // loaded from /api/profile-fields profileScope: 'model', // 'preset' | 'global' | 'model' refreshingPresets: false, activeProfileName: null, // currently-active profile for the form profilesDrift: false, // true if form values differ from active profile _applySeq: 0, // monotonic counter for apply race guard profileError: '', showNewProfileForm: false, newProfile: { display_name: '', api_name: '', api_name_touched: false, description: '', also_as_template: false }, showNewTemplateForm: false, newTemplate: { name: '', display_name: '', description: '' }, editingProfile: null, // profile name being edited inline editingTemplate: null, // template name being edited inline profileDeleteConfirm: null, templateDeleteConfirm: null, // Status tab state stats: { total_prompt_tokens: 0, total_cached_tokens: 0, cache_efficiency: 0.0, avg_prefill_tps: 0.0, avg_generation_tps: 0.0, total_requests: 0, host: '127.0.0.1', port: 8000, api_key: '', engines: {}, active_models: { models: [], model_memory_used: 0, model_memory_max: 0, memory_pressure: { enabled: false, current_bytes: 0, soft_bytes: 0, hard_bytes: 0, pressure_level: 'ok', }, total_active_requests: 0, total_waiting_requests: 0, }, runtime_cache: { base_path: '', ssd_cache_dir: '', response_state_dir: '', models: [], total_num_files: 0, total_size_bytes: 0, effective_block_sizes: [], hot_cache_size_bytes: 0, hot_cache_entries: 0, hot_cache_max_bytes: 0, disk_max_bytes: 0, }, }, alltimeStats: { total_prompt_tokens: 0, total_cached_tokens: 0, cache_efficiency: 0.0, avg_prefill_tps: 0.0, avg_generation_tps: 0.0, total_requests: 0, }, // Server connectivity info (from /admin/api/server-info) serverAliases: [], selectedAlias: '', // Server-restart state machine (driven by Settings > Server > Restart). // status transitions: idle → restarting → waiting → idle (success) // | | // | └─→ error (timeout / non-200) // └─→ unsupported (no menubar supervisor) // └─→ error (POST failed) restartServer: { status: 'idle', message: '', }, statsScope: 'session', selectedStatsModel: '', showClearStatsConfirm: false, showClearAlltimeConfirm: false, showClearSsdCacheConfirm: false, showClearHotCacheConfirm: false, _statsRefreshTimer: null, // Log viewer state logContent: '', logLines: 500, logRefreshInterval: 5, // seconds, 0 = disabled logAutoRefresh: false, logAutoScroll: true, logLoading: false, logError: '', logFile: 'server.log', logAvailableFiles: ['server.log'], logTotalLines: 0, logLastUpdated: '', logMinLevel: 'TRACE', _logRefreshTimer: null, // Models sub-tab state modelsTab: 'manager', modelsDropdown: false, // HF Mirror settings modal showHfMirrorModal: false, hfMirrorEndpoint: '', hfMirrorSaving: false, // Update check state updateAvailable: false, latestVersion: null, releaseUrl: null, versionHover: false, _updateCheckTimer: null, // HF Downloader state hfRepoId: '', hfToken: '', hfDownloading: false, hfTasks: [], hfModels: [], hfModelsLoaded: false, hfError: '', hfSuccess: '', _hfRefreshTimer: null, hfDeleteConfirm: null, // Recommended models state hfRecommended: { trending: [], popular: [] }, hfRecommendedLoaded: false, hfRecommendedLoading: false, hfRecommendedTab: 'trending', hfMlxOnly: true, // Pagination state hfPage: { trending: 1, popular: 1, search: 1 }, hfPageSize: 10, // Search state hfSearchQuery: '', hfSearchSort: 'downloads', hfSearchResults: [], hfSearchLoading: false, hfSearchLoaded: false, hfSearchDebounceTimer: null, // Search filters hfSearchFiltersOpen: false, hfSearchMinParams: '', hfSearchMaxParams: '', hfSearchMaxSize: '', hfSearchMinSize: '', // Table sort state for Browse Models hfTableSort: 'downloads', hfTableSortDir: 'desc', // Computed: check if any filters are active get hfSearchFiltersActive() { return this.hfSearchMinParams || this.hfSearchMaxParams || this.hfSearchMaxSize || this.hfSearchMinSize; }, // Search history hfSearchHistory: JSON.parse(localStorage.getItem('hfSearchHistory') || '[]'), hfSearchHistoryOpen: false, // Model detail modal hfModelDetail: null, hfModelDetailLoading: false, // ModelScope Downloader state downloaderSource: 'hf', msAvailable: false, msInitialized: false, msRepoId: '', msToken: '', msDownloading: false, msTasks: [], msError: '', msSuccess: '', _msRefreshTimer: null, // MS Recommended models state msRecommended: { trending: [], popular: [] }, msRecommendedLoaded: false, msRecommendedLoading: false, msRecommendedTab: 'trending', msMlxOnly: true, // MS Pagination state msPage: { trending: 1, popular: 1, search: 1 }, msPageSize: 10, // MS Search state msSearchQuery: '', msSearchSort: 'trending', msSearchResults: [], msSearchLoading: false, msSearchLoaded: false, msSearchHistory: JSON.parse(localStorage.getItem('msSearchHistory') || '[]'), msSearchHistoryOpen: false, msSearchDebounceTimer: null, // MS Model detail modal msModelDetail: null, msModelDetailLoading: false, // oQ Quantizer state oqModels: [], oqAllModels: [], oqModelsLoaded: false, oqSelectedModelPath: '', oqLevel: 4, oqStarting: false, oqTasks: [], oqError: '', oqSuccess: '', _oqRefreshTimer: null, // oQ Advanced Settings oqAdvancedOpen: false, oqTextOnly: false, oqDtype: 'bfloat16', oqSensitivityModelPath: '', oqPreserveMtp: false, oqEnhanced: false, oqeReuseImatrixCache: true, oqeImatrixCachePath: '', oqeStrictImatrix: false, // oQ Uploader state uploadHfToken: localStorage.getItem('omlx-hf-upload-token') || '', uploadHfUsername: '', uploadHfOrgs: [], uploadHfNamespace: '', uploadTokenValidated: false, uploadTokenValidating: false, uploadOqModels: [], uploadAllModels: [], uploadOqModelsLoaded: false, uploadTasks: [], uploadError: '', uploadSuccess: '', _uploadRefreshTimer: null, // Upload modal uploadModalOpen: false, uploadModalModelPath: '', uploadModalModelName: '', uploadModalRepoId: '', uploadReadmeSource: '', uploadAutoReadme: true, uploadPrivate: false, uploadStarting: false, // Benchmark state benchModelId: '', benchPromptLengths: { 1024: true, 4096: true, 8192: false, 16384: false, 32768: false, 65536: false, 131072: false, 200000: false }, benchBatchSizes: { 2: true, 4: true, 8: false }, benchForceLmEngine: false, benchAdvancedOptionsOpen: false, benchExternalEnabled: false, // Shared external endpoint settings (persisted in localStorage, // used by both the throughput and accuracy bench tabs) externalBaseUrl: localStorage.getItem('omlx_bench_external_base_url') || '', externalApiKey: localStorage.getItem('omlx_bench_external_api_key') || '', externalModel: localStorage.getItem('omlx_bench_external_model') || '', // { base_url, model } snapshot of the current run when external // (no API key — used for the text export header) benchRunExternal: null, benchRunning: false, benchBenchId: null, benchProgress: null, benchSingleResults: [], benchBatchResults: [], benchError: '', benchEventSource: null, benchShowMetrics: false, benchShowText: false, benchCopied: false, benchTip: null, benchDeviceInfo: null, benchUploadResults: [], benchUploadDone: null, benchUploading: false, benchUploadSkipped: null, // { features: [...] } when upload was skipped due to experimental features // { bench_id, model_id } when the server reports a running bench // that is NOT the one this tab is displaying. Drives the "another // bench is running" banner + disables Start so the user doesn't // race a 409 on the server. benchOtherActive: null, // Bench sub-tab & dropdown benchTab: 'throughput', benchDropdown: false, // Accuracy benchmark state accModelId: '', accBenchmarks: { mmlu: true, mmlu_pro: false, kmmlu: false, cmmlu: false, jmmlu: false, hellaswag: false, truthfulqa: true, arc_challenge: false, winogrande: false, gsm8k: false, mathqa: false, humaneval: true, mbpp: false, livecodebench: false, bbq: false, safetybench: false }, accSampleSizes: { mmlu: 1000, mmlu_pro: 300, kmmlu: 300, cmmlu: 300, jmmlu: 300, hellaswag: 200, truthfulqa: 0, arc_challenge: 300, winogrande: 300, gsm8k: 100, mathqa: 300, humaneval: 0, mbpp: 200, livecodebench: 100, bbq: 300, safetybench: 300 }, accBenchmarkGroups: [ { name: 'Knowledge', benchmarks: [ { key: 'mmlu', label: 'MMLU', desc: 'Knowledge · 57 subjects', fullSize: 14042, sizes: [30, 50, 100, 200, 300, 500, 1000, 2000] }, { key: 'mmlu_pro', label: 'MMLU-Pro', desc: 'Hard knowledge · 14 subjects (10-way)', fullSize: 12032, sizes: [30, 50, 100, 200, 300, 500, 1000, 2000] }, { key: 'kmmlu', label: 'KMMLU', desc: '한국어 지식 · 45 과목', fullSize: 35030, sizes: [30, 50, 100, 200, 300, 500, 1000, 2000] }, { key: 'cmmlu', label: 'CMMLU', desc: '中文知识 · 67 科目', fullSize: 11582, sizes: [30, 50, 100, 200, 300, 500, 1000, 2000] }, { key: 'jmmlu', label: 'JMMLU', desc: '日本語知識 · 112 科目', fullSize: 7536, sizes: [30, 50, 100, 200, 300, 500, 1000, 2000] }, ], }, { name: 'Commonsense & Reasoning', benchmarks: [ { key: 'hellaswag', label: 'HellaSwag', desc: 'Commonsense reasoning', fullSize: 10042, sizes: [30, 50, 100, 200, 300, 500, 1000, 2000] }, { key: 'arc_challenge', label: 'ARC-C', desc: 'Science reasoning', fullSize: 1172, sizes: [30, 50, 100, 200, 300] }, { key: 'winogrande', label: 'Winogrande', desc: 'Coreference resolution', fullSize: 1267, sizes: [30, 50, 100, 200, 300] }, { key: 'truthfulqa', label: 'TruthfulQA', desc: 'Truthfulness', fullSize: 817, sizes: [30, 50, 100, 200, 300] }, ], }, { name: 'Math', benchmarks: [ { key: 'gsm8k', label: 'GSM8K', desc: 'Math reasoning', fullSize: 1319, sizes: [30, 50, 100, 200, 300] }, { key: 'mathqa', label: 'MathQA', desc: 'Quantitative reasoning · 5-way', fullSize: 2985, sizes: [30, 50, 100, 200, 300, 500, 1000] }, ], }, { name: 'Coding', benchmarks: [ { key: 'humaneval', label: 'HumanEval', desc: 'Function completion', fullSize: 164, sizes: [30, 50, 100] }, { key: 'mbpp', label: 'MBPP', desc: 'Python problems', fullSize: 500, sizes: [30, 50, 100, 200, 300] }, { key: 'livecodebench', label: 'LiveCodeBench', desc: 'Code generation', fullSize: 1055, sizes: [30, 50, 100, 200, 300] }, ], }, { name: 'Safety & Alignment', benchmarks: [ { key: 'bbq', label: 'BBQ', desc: 'Social bias · 11 categories', fullSize: 10864, sizes: [30, 50, 100, 200, 300, 500, 1000, 2000] }, { key: 'safetybench', label: 'SafetyBench', desc: 'Safety · 7 categories', fullSize: 11435, sizes: [30, 50, 100, 200, 300, 500, 1000, 2000] }, ], }, ], accBatchSize: 1, accEnableThinking: false, accSamplingProfile: 'deterministic', accAdvancedOptionsOpen: false, accExternalEnabled: false, accRunning: false, accCurrentModel: '', accCurrentBenchId: null, accProgress: null, accAllResults: [], // accumulated across all models accQueue: [], // server queue mirror accError: '', accEventSource: null, accShowText: false, accCopied: false, async init() { // Apply theme this.applyTheme(); this.applyTabStateFromUrl(); await Promise.all([ this.loadGlobalSettings(), this.loadModels(), this.loadServerInfo(), this.loadProfileFields(), this.loadPresets(), this.checkForUpdate() ]); this.startUpdateCheckTimer(); await this.handleMainTabChange(this.mainTab); // Watch for main tab changes to manage refresh timers this.$watch('mainTab', (value) => { this.handleMainTabChange(value); }); // When the user returns to this browser tab after looking // elsewhere, re-check whether a different bench just started // in another tab. Fires the banner without requiring an // in-app tab switch. document.addEventListener('visibilitychange', () => { if (document.visibilityState !== 'visible') return; if (this.mainTab === 'bench' && this.benchTab === 'throughput') { this.loadBenchState(); } }); this.$watch('hfMlxOnly', () => { this.hfRecommended = { trending: [], popular: [] }; this.hfRecommendedLoaded = false; this.hfSearchResults = []; this.hfSearchLoaded = false; this.loadRecommendedModels(); if (this.hfSearchQuery.trim()) { this.searchHFModels(); } }); this.$watch('msMlxOnly', () => { this.msRecommended = { trending: [], popular: [] }; this.msRecommendedLoaded = false; this.msSearchResults = []; this.msSearchLoaded = false; this.loadMsRecommendedModels(); if (this.msSearchQuery.trim()) { this.searchMSModels(); } }); window.addEventListener('popstate', () => { this.applyTabStateFromUrl(); }); // Pause stats polling when tab is hidden to reduce server load document.addEventListener('visibilitychange', () => { if (document.hidden) { this.stopStatsRefresh(); } else if (this.mainTab === 'status') { this.loadStats(); this.startStatsRefresh(); } }); }, async handleMainTabChange(value) { if (value === 'status') { await this.loadStats(); this.startStatsRefresh(); } else { this.stopStatsRefresh(); } if (value === 'logs') { await this.loadLogs(); this.startLogRefresh(); } else { this.stopLogRefresh(); } if (value === 'models') { const loads = [this.loadHFModels(), this.loadHFTasks(), this.loadOQTasks()]; if (this.modelsTab === 'downloader' && !this.hfRecommendedLoaded) { loads.push(this.loadRecommendedModels()); } if (this.modelsTab === 'quantizer') { loads.push(this.loadOQModels()); } if (this.msInitialized && this.msAvailable) { loads.push(this.loadMSTasks()); } await Promise.all(loads); const hasActive = this.hfTasks.some(t => t.status === 'pending' || t.status === 'downloading'); if (hasActive) this.startHFRefresh(); const hasMsActive = this.msTasks.some(t => t.status === 'pending' || t.status === 'downloading'); if (hasMsActive) this.startMSRefresh(); const hasOqActive = this.oqTasks.some(t => ['pending', 'loading', 'quantizing', 'saving'].includes(t.status)); if (hasOqActive) this.startOQRefresh(); } else { this.stopHFRefresh(); this.stopMSRefresh(); this.stopOQRefresh(); } if (value === 'bench') { if (!this.benchDeviceInfo) await this.loadBenchDeviceInfo(); await this.loadBenchState(); await this.loadAccState(); } }, applyTabStateFromUrl() { const params = new URLSearchParams(window.location.search); const mainTab = params.get('tab'); const settingsTab = params.get('settingsTab'); const modelsTab = params.get('modelsTab'); const benchTab = params.get('benchTab'); this.mainTab = DASHBOARD_MAIN_TABS.has(mainTab) ? mainTab : 'status'; this.activeTab = DASHBOARD_SETTINGS_TABS.has(settingsTab) ? settingsTab : 'global'; this.modelsTab = DASHBOARD_MODELS_TABS.has(modelsTab) ? modelsTab : 'manager'; this.benchTab = DASHBOARD_BENCH_TABS.has(benchTab) ? benchTab : 'throughput'; }, syncTabStateToUrl() { const url = new URL(window.location.href); url.searchParams.set('tab', this.mainTab); if (this.mainTab === 'settings') { url.searchParams.set('settingsTab', this.activeTab); } else { url.searchParams.delete('settingsTab'); } if (this.mainTab === 'models') { url.searchParams.set('modelsTab', this.modelsTab); } else { url.searchParams.delete('modelsTab'); } if (this.mainTab === 'bench') { url.searchParams.set('benchTab', this.benchTab); } else { url.searchParams.delete('benchTab'); } window.history.replaceState({}, '', url); }, setMainTab(tab) { if (!DASHBOARD_MAIN_TABS.has(tab)) return; this.mainTab = tab; this.syncTabStateToUrl(); }, setSettingsTab(tab) { if (!DASHBOARD_SETTINGS_TABS.has(tab)) return; this.activeTab = tab; this.mainTab = 'settings'; this.syncTabStateToUrl(); }, setModelsTab(tab) { if (!DASHBOARD_MODELS_TABS.has(tab)) return; this.modelsTab = tab; this.mainTab = 'models'; this.syncTabStateToUrl(); if (tab === 'quantizer') { this.loadOQModels(); } if (tab === 'uploader') { if (!this.uploadOqModelsLoaded) this.loadUploadOqModels(); this.loadUploadTasks(); } }, async checkForUpdate() { try { const resp = await fetch('/admin/api/update-check'); if (resp.ok) { const data = await resp.json(); this.updateAvailable = data.update_available; this.latestVersion = data.latest_version; this.releaseUrl = data.release_url; } } catch (e) { // Silently ignore - not critical } }, startUpdateCheckTimer() { this._updateCheckTimer = setInterval(() => this.checkForUpdate(), 3600000); }, stopUpdateCheckTimer() { if (this._updateCheckTimer) { clearInterval(this._updateCheckTimer); this._updateCheckTimer = null; } }, async loadGlobalSettings() { try { const response = await fetch('/admin/api/global-settings'); if (response.ok) { const data = await response.json(); // Deep merge to preserve defaults for missing fields // Handle model_dirs: prefer list, fallback to single model_dir const modelDirs = data.model?.model_dirs?.length ? data.model.model_dirs : (data.model?.model_dir ? [data.model.model_dir] : ['']); this.globalSettings = { ...this.globalSettings, ...data, server: { ...this.globalSettings.server, ...data.server }, model: { ...this.globalSettings.model, ...data.model, model_dirs: modelDirs }, memory: { ...this.globalSettings.memory, ...data.memory }, scheduler: { ...this.globalSettings.scheduler, ...data.scheduler }, cache: { ...this.globalSettings.cache, ...data.cache }, sampling: { ...this.globalSettings.sampling, ...data.sampling }, mcp: { ...this.globalSettings.mcp, ...data.mcp }, huggingface: { ...this.globalSettings.huggingface, ...data.huggingface }, network: { ...this.globalSettings.network, ...data.network }, auth: { ...this.globalSettings.auth, ...data.auth }, claude_code: { ...this.globalSettings.claude_code, ...data.claude_code }, integrations: { ...this.globalSettings.integrations, ...data.integrations }, idle_timeout: { ...this.globalSettings.idle_timeout, ...data.idle_timeout }, system: { ...this.globalSettings.system, ...data.system }, }; this.globalSettings.ui = data.ui || { language: 'en' }; // Sync idle timeout select value this.idleTimeoutValue = this.globalSettings.idle_timeout?.idle_timeout_seconds != null ? String(this.globalSettings.idle_timeout.idle_timeout_seconds) : ''; // Normalize memory guard tier to one of the known values. const validTiers = ['safe', 'balanced', 'aggressive', 'custom']; if (!validTiers.includes(this.globalSettings.memory.memory_guard_tier)) { this.globalSettings.memory.memory_guard_tier = 'balanced'; } // Calculate cache percent from stored value (based on total capacity) this.cachePercent = this.parseCacheToPercent( this.globalSettings.cache.ssd_cache_max_size, this.globalSettings.system.ssd_total_bytes ); // Sync the cache string value from percent this.updateCacheFromSlider(); // Calculate hot cache percent from stored value this.globalSettings.cache.hot_cache_max_size = this.normalizeHotCacheMaxSize( this.globalSettings.cache.hot_cache_max_size ); this.hotCachePercent = this.parseHotCacheToPercent( this.globalSettings.cache.hot_cache_max_size, this.globalSettings.system.total_memory_bytes ); } else if (response.status === 401) { window.location.href = '/admin'; } } catch (err) { console.error('Failed to load global settings:', err); } }, async saveGlobalSettings() { this.saving = true; this.saveSuccess = false; this.saveError = ''; // Validate required fields const errors = []; const s = this.globalSettings; if (!s.server.host) errors.push('Host'); if (!s.server.port) errors.push('Port'); if (!s.model.model_dirs || !s.model.model_dirs.some(d => d.trim())) errors.push('Model Directory'); if (!s.scheduler.max_concurrent_requests) errors.push('Max Concurrent Requests'); if (!s.scheduler.embedding_batch_size) errors.push('Embedding Batch Size'); if (!s.cache.ssd_cache_max_size) errors.push('Max Cache Size'); if (!s.sampling.max_context_window) errors.push('Max Context Window'); if (!s.sampling.max_tokens) errors.push('Max Tokens'); if (errors.length > 0) { this.saveError = window.t('js.error.required_fields').replace('{fields}', errors.join(', ')); this.saving = false; return; } // Validate API key if provided if (s.auth.api_key) { if (s.auth.api_key.length < 4) { this.saveError = window.t('js.error.api_key_min_length'); this.saving = false; return; } if (/\s/.test(s.auth.api_key)) { this.saveError = window.t('js.error.api_key_no_whitespace'); this.saving = false; return; } } try { const response = await fetch('/admin/api/global-settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ host: this.globalSettings.server.host, port: this.globalSettings.server.port, log_level: this.globalSettings.server.log_level, sse_keepalive_mode: this.globalSettings.server.sse_keepalive_mode, burst_decode_mode: this.globalSettings.server.burst_decode_mode, preserve_mid_system_cache: this.globalSettings.server.preserve_mid_system_cache, model_dirs: this.globalSettings.model.model_dirs.filter(d => d.trim()), model_fallback: this.globalSettings.model.model_fallback, hide_helper_models: this.globalSettings.model.hide_helper_models, memory_prefill_memory_guard: this.globalSettings.memory.prefill_memory_guard, memory_guard_tier: this.globalSettings.memory.memory_guard_tier, memory_guard_custom_ceiling_gb: this.globalSettings.memory.memory_guard_custom_ceiling_gb, max_concurrent_requests: this.globalSettings.scheduler.max_concurrent_requests, embedding_batch_size: this.globalSettings.scheduler.embedding_batch_size, chunked_prefill: this.globalSettings.scheduler.chunked_prefill, cache_enabled: this.globalSettings.cache.enabled, ssd_cache_dir: this.globalSettings.cache.ssd_cache_dir, ssd_cache_max_size: this.globalSettings.cache.ssd_cache_max_size, hot_cache_max_size: this.normalizeHotCacheMaxSize( this.globalSettings.cache.hot_cache_max_size ), initial_cache_blocks: this.globalSettings.cache.initial_cache_blocks, hot_cache_only: this.globalSettings.cache.hot_cache_only, sampling_max_context_window: this.globalSettings.sampling.max_context_window, sampling_max_context_window_policy: this.globalSettings.sampling.max_context_window_policy || null, sampling_max_tokens: this.globalSettings.sampling.max_tokens, sampling_temperature: this.globalSettings.sampling.temperature, sampling_top_p: this.globalSettings.sampling.top_p, sampling_top_k: this.globalSettings.sampling.top_k, sampling_repetition_penalty: this.globalSettings.sampling.repetition_penalty, mcp_config: this.globalSettings.mcp.config_path, hf_cache_enabled: this.globalSettings.huggingface.hf_cache_enabled, network_http_proxy: this.globalSettings.network.http_proxy, network_https_proxy: this.globalSettings.network.https_proxy, network_no_proxy: this.globalSettings.network.no_proxy, network_ca_bundle: this.globalSettings.network.ca_bundle, ...(this.globalSettings.auth.api_key ? { api_key: this.globalSettings.auth.api_key } : {}), skip_api_key_verification: this.globalSettings.auth.skip_api_key_verification, idle_timeout_seconds: this.globalSettings.idle_timeout?.idle_timeout_seconds ?? null, }), }); if (response.ok) { const data = await response.json(); this.saveSuccess = true; this.saveMessage = data.message || 'Settings saved successfully'; // Refresh stats and model list (cache changes unload models) await this.loadStats(); await this.loadModels(); setTimeout(() => { this.saveSuccess = false; }, 5000); } else if (response.status === 401) { window.location.href = '/admin'; } else { const data = await response.json(); this.saveError = Array.isArray(data.detail) ? data.detail.map(e => (e && typeof e === 'object') ? (e.msg || JSON.stringify(e)) : String(e)).join(', ') : (data.detail || window.t('js.error.save_settings_failed')); // Reload settings to revert to server values await this.loadGlobalSettings(); } } catch (err) { console.error('Failed to save global settings:', err); this.saveError = window.t('js.error.save_settings_failed'); // Reload settings to revert to server values await this.loadGlobalSettings(); } finally { this.saving = false; } }, // Sub key management generateSubKey() { const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; const rand = Array.from(crypto.getRandomValues(new Uint8Array(16))) .map(b => chars[b % chars.length]).join(''); this.newSubKeyValue = 'omlx-' + rand; this.showNewSubKey = true; }, async createSubKey() { this.subKeyError = ''; if (!this.newSubKeyValue || this.newSubKeyValue.length < 4) { this.subKeyError = window.t('js.error.api_key_min_length'); return; } if (/\s/.test(this.newSubKeyValue)) { this.subKeyError = window.t('js.error.api_key_no_whitespace'); return; } try { const response = await fetch('/admin/api/sub-keys', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key: this.newSubKeyValue, name: this.newSubKeyName }), }); if (response.ok) { this.newSubKeyValue = ''; this.newSubKeyName = ''; this.showNewSubKeyForm = false; this.showNewSubKey = false; await this.loadGlobalSettings(); } else if (response.status === 401) { window.location.href = '/admin'; } else { const data = await response.json(); this.subKeyError = data.detail || window.t('js.error.save_settings_failed'); } } catch (err) { this.subKeyError = window.t('js.error.save_settings_failed'); } }, async deleteSubKey(key) { if (!confirm(window.t('settings.auth.sub_keys_delete_confirm'))) return; try { const response = await fetch('/admin/api/sub-keys', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key }), }); if (response.ok) { await this.loadGlobalSettings(); } else if (response.status === 401) { window.location.href = '/admin'; } } catch (err) { console.error('Failed to delete sub key:', err); } }, async loadModels() { this.loadingModels = true; try { const response = await fetch('/admin/api/models'); if (response.ok) { const data = await response.json(); this.models = data.models || []; } else if (response.status === 401) { window.location.href = '/admin'; } } catch (err) { console.error('Failed to load models:', err); } finally { this.loadingModels = false; } }, async reloadModels() { if (this.reloading) return; this.reloading = true; try { const response = await fetch('/admin/api/reload', { method: 'POST' }); if (response.ok) { await Promise.all([this.loadModels(), this.loadHFModels()]); } else if (response.status === 401) { window.location.href = '/admin'; } else { const data = await response.json(); alert(data.detail || window.t('js.error.reload_failed')); } } catch (err) { console.error('Failed to reload models:', err); alert(window.t('js.error.reload_failed')); } finally { this.reloading = false; } }, async updateModelSetting(modelId, field, value) { try { const response = await fetch(`/admin/api/models/${encodeURIComponent(modelId)}/settings`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ [field]: value }), }); if (response.ok) { if (field === 'is_default' && value === true) { this.models.forEach(m => { m.is_default = (m.id === modelId); }); } else if (field === 'is_pinned') { const model = this.models.find(m => m.id === modelId); if (model) model.pinned = value; } else if (field === 'is_hidden') { const model = this.models.find(m => m.id === modelId); if (model) model.is_hidden = value; } else if (field === 'is_favorite') { const model = this.models.find(m => m.id === modelId); if (model) model.is_favorite = value; } } else if (response.status === 401) { window.location.href = '/admin'; } else { const data = await response.json(); alert(data.detail || window.t('js.error.update_model_setting_failed')); await this.loadModels(); } } catch (err) { console.error('Failed to update model setting:', err); alert(window.t('js.error.update_model_setting_failed')); await this.loadModels(); } }, async loadModel(modelId) { const model = this.models.find(m => m.id === modelId); if (model) model.is_loading = true; try { const response = await fetch(`/admin/api/models/${encodeURIComponent(modelId)}/load`, { method: 'POST', }); if (response.ok) { await this.loadModels(); } else if (response.status === 401) { window.location.href = '/admin'; } else { const data = await response.json(); alert(data.detail || window.t('js.error.load_model_failed')); await this.loadModels(); } } catch (err) { console.error('Failed to load model:', err); alert(window.t('js.error.load_model_failed')); await this.loadModels(); } }, async unloadModel(modelId) { try { const response = await fetch(`/admin/api/models/${encodeURIComponent(modelId)}/unload`, { method: 'POST', }); if (response.ok) { const model = this.models.find(m => m.id === modelId); if (model) model.loaded = false; } else if (response.status === 401) { window.location.href = '/admin'; } else { const data = await response.json(); alert(data.detail || window.t('js.error.unload_model_failed')); } await this.loadModels(); } catch (err) { console.error('Failed to unload model:', err); alert(window.t('js.error.unload_model_failed')); await this.loadModels(); } }, // ===== Profiles / Templates ===== formValuesForProfile() { const ms = this.modelSettings; const out = {}; const isDiffusion = !!ms.is_diffusion_model; for (const k of this.profileFields.universal.concat(this.profileFields.model_specific)) { if (k === 'chat_template_kwargs' || k === 'forced_ct_kwargs') continue; // handle below if (isDiffusion && this.isDiffusionUnsupportedProfileField(k)) continue; if (k === 'thinking_budget_enabled') { if (ms.enableThinkingBudget) out.thinking_budget_tokens = ms.thinking_budget_tokens ?? null; continue; } if (k === 'index_cache_freq') { if (ms.enableIndexCache) out.index_cache_freq = ms.index_cache_freq || 4; continue; } if (k === 'max_tool_result_tokens') { if (ms.enableToolResultLimit) out.max_tool_result_tokens = ms.max_tool_result_tokens || null; continue; } if (k === 'guided_grammar_enabled') { out.guided_grammar_enabled = !!ms.guided_grammar_enabled; continue; } if (k === 'guided_grammar') { out.guided_grammar = ms.guided_grammar_enabled ? ((ms.guided_grammar || '').trim() || null) : null; continue; } // Standard field: apply nullish coalescing; coerce string numerics let v = ms[k] ?? null; if (typeof v === 'string' && v !== '' && !isNaN(Number(v))) v = Number(v); out[k] = v; } // Build chat_template_kwargs and forced_ct_kwargs from ctKwargEntries const ctk = {}; const forced = []; for (const e of (ms.ctKwargEntries || [])) { if (e.type === 'enable_thinking') { if (isDiffusion) continue; ctk.enable_thinking = e.value === 'true'; if (e.force) forced.push('enable_thinking'); } else if (e.type === 'reasoning_effort') { if (isDiffusion) continue; ctk.reasoning_effort = e.value; if (e.force) forced.push('reasoning_effort'); } else if (e.type === 'custom' && e.key && e.key.trim()) { if (isDiffusion && this.isDiffusionUnsupportedCtKwarg(e.key.trim())) { continue; } let v = e.value; if (v === 'true') v = true; else if (v === 'false') v = false; else if (!isNaN(Number(v)) && String(v).trim() !== '') v = Number(v); ctk[e.key.trim()] = v; if (e.force) forced.push(e.key.trim()); } } if (Object.keys(ctk).length > 0) out.chat_template_kwargs = ctk; if (forced.length > 0) out.forced_ct_kwargs = forced; return out; }, formValuesForTemplate() { const full = this.formValuesForProfile(); const out = {}; for (const k of this.profileFields.universal) { if (k in full) out[k] = full[k]; } return out; }, computeDrift() { if (!this.activeProfileName) { this.profilesDrift = false; return; } const active = this.profiles.find(p => p.name === this.activeProfileName); if (!active) { this.profilesDrift = false; return; } const form = this.formValuesForProfile(); for (const [k, v] of Object.entries(active.settings || {})) { if (JSON.stringify(form[k]) !== JSON.stringify(v)) { this.profilesDrift = true; return; } } this.profilesDrift = false; }, matchedPreset(settings) { // Return the preset whose universal-field settings match the current model // settings exactly, otherwise null. Used by the models list to show "which // preset was applied" as a pill without any server-side tracking. if (!settings || !this.presets || this.presets.length === 0) return null; const universal = this.profileFields.universal || []; if (universal.length === 0) return null; const canonical = v => { if (v === undefined || v === null || v === false) return null; if (typeof v === 'object') { return JSON.stringify(v, Object.keys(v).sort()); } return v; }; for (const p of this.presets) { const ps = p.settings || {}; let ok = true; for (const k of universal) { if (canonical(ps[k]) !== canonical(settings[k])) { ok = false; break; } } if (ok) return p; } return null; }, profileTooltip(profile) { const lines = []; if (profile?.expose_as_model && profile.model_id) { lines.push(profile.model_id); } const description = (profile?.description || '').trim(); if (description) lines.push(description); return lines.join('\n'); }, async loadProfilesForModel(modelId) { this.profiles = []; try { const r = await fetch(`/admin/api/models/${encodeURIComponent(modelId)}/profiles`); if (r.ok) { const data = await r.json(); this.profiles = data.profiles || []; } else if (r.status === 401) { window.location.href = '/admin'; } } catch (e) { console.error('Failed to load profiles:', e); } }, async loadTemplates() { try { const r = await fetch('/admin/api/profile-templates'); if (r.ok) { const data = await r.json(); this.templates = data.templates || []; } else if (r.status === 401) { window.location.href = '/admin'; } } catch (e) { console.error('Failed to load templates:', e); } }, async loadProfileFields() { try { const r = await fetch('/admin/api/profile-fields'); if (r.ok) { const data = await r.json(); this.profileFields = { universal: data.universal || [], model_specific: data.model_specific || [], }; } else if (r.status === 401) { window.location.href = '/admin'; } } catch (e) { console.error('Failed to load profile field definitions:', e); } }, async loadPresets() { // Use localStorage cache if present, otherwise fall back to the bundled file. const cached = localStorage.getItem('omlx_preset_cache'); if (cached) { try { const parsed = JSON.parse(cached); this.presets = parsed.presets || []; return; } catch (e) { /* corrupted, fall through */ } } try { const r = await fetch('/admin/static/omlx_preset.json'); if (r.ok) { const data = await r.json(); this.presets = data.presets || []; } } catch (e) { console.error('Failed to load bundled presets:', e); } }, async refreshPresets() { if (this.refreshingPresets) return; this.refreshingPresets = true; try { const r = await fetch('/admin/api/presets/refresh', { method: 'POST' }); if (r.ok) { const data = await r.json(); this.presets = data.presets || []; localStorage.setItem('omlx_preset_cache', JSON.stringify(data)); } else if (r.status === 401) { window.location.href = '/admin'; } } catch (e) { console.error('Preset refresh failed:', e); } finally { this.refreshingPresets = false; } }, // Floating tooltip shared by preset/profile pills (position:fixed escapes // the scroll container's overflow clipping, unlike absolute+group-hover). tip: { visible: false, text: '', x: 0, y: 0 }, showTip(el, text) { if (!text) return; const rect = el.getBoundingClientRect(); this.tip = { visible: true, text: text, x: rect.left + rect.width / 2, y: rect.bottom + 6, }; }, hideTip() { this.tip.visible = false; }, isDiffusionModel(model) { const modelType = String(model?.config_model_type || '') .toLowerCase() .replace(/-/g, '_'); return DIFFUSION_CONFIG_MODEL_TYPES.has(modelType); }, isDiffusionUnsupportedProfileField(field) { return DIFFUSION_UNSUPPORTED_PROFILE_FIELDS.has(field); }, isDiffusionUnsupportedCtKwarg(key) { return DIFFUSION_UNSUPPORTED_CT_KWARGS.has(key); }, draftModelSearchText(model) { return [ model?.id, model?.name, model?.model_path, model?.source_repo_id, model?.config_model_type, ].filter(Boolean).join(' ').toLowerCase(); }, isDraftModelBaseCandidate(model) { if (!model || model.virtual) return false; if (model.id === this.selectedModel?.id) return false; return model.model_type === 'llm' || model.model_type === 'vlm' || !model.model_type; }, isDflashDraftModel(model) { return /(^|[-_/\s])dflash($|[-_/\s])/i.test(this.draftModelSearchText(model)); }, isVlmMtpDraftModel(model) { const configType = String(model?.config_model_type || '').toLowerCase(); if (configType) { return VLM_MTP_DRAFTER_CONFIG_MODEL_TYPES.has(configType); } return /assistant|(^|[-_/\s])mtp($|[-_/\s])/i.test(this.draftModelSearchText(model)); }, isSpecPrefillDraftModel(model) { return !this.isDflashDraftModel(model) && !this.isVlmMtpDraftModel(model); }, draftModelCandidates(filterFn, { fallbackToBase = true } = {}) { const base = (this.models || []).filter((model) => ( this.isDraftModelBaseCandidate(model) )); const filtered = base.filter(filterFn); return (filtered.length > 0 || !fallbackToBase) ? filtered : base; }, specPrefillDraftModelCandidates() { return this.draftModelCandidates((model) => this.isSpecPrefillDraftModel(model)); }, dflashDraftModelCandidates() { return this.draftModelCandidates((model) => this.isDflashDraftModel(model)); }, vlmMtpDraftModelCandidates() { return this.draftModelCandidates( (model) => this.isVlmMtpDraftModel(model), { fallbackToBase: false }, ); }, buildCtKwargEntries(chatTemplateKwargs, forcedCtKwargs, isDiffusion = false) { const ctk = chatTemplateKwargs || {}; const forced = new Set(forcedCtKwargs || []); const entries = []; for (const [key, value] of Object.entries(ctk)) { if (isDiffusion && this.isDiffusionUnsupportedCtKwarg(key)) { continue; } if (key === 'enable_thinking') { entries.push({ type: 'enable_thinking', value: String(value), force: forced.has('enable_thinking'), }); } else if (key === 'reasoning_effort') { entries.push({ type: 'reasoning_effort', value: String(value), force: forced.has('reasoning_effort'), }); } else { entries.push({ type: 'custom', key, value: String(value), force: forced.has(key), }); } } return entries; }, buildModelSettingsState(model, settings) { const s = settings || {}; const isDiffusion = this.isDiffusionModel(model); const ctKwargEntries = this.buildCtKwargEntries( s.chat_template_kwargs, s.forced_ct_kwargs, isDiffusion, ); const isOcr = OCR_CONFIG_MODEL_TYPES.has(model?.config_model_type || ''); return { model_alias: s.model_alias || '', model_type_override: s.model_type_override || '', max_context_window: s.max_context_window || null, max_tokens: s.max_tokens || null, temperature: isOcr ? 0.0 : (s.temperature ?? null), top_p: s.top_p ?? null, top_k: s.top_k ?? null, repetition_penalty: s.repetition_penalty ?? null, min_p: s.min_p ?? null, presence_penalty: s.presence_penalty ?? null, force_sampling: s.force_sampling || false, enable_thinking: s.enable_thinking ?? null, thinking_default: model?.thinking_default ?? null, enableThinkingBudget: !!(s.thinking_budget_tokens), thinking_budget_tokens: s.thinking_budget_tokens || null, guided_grammar_enabled: s.guided_grammar_enabled || false, guided_grammar: s.guided_grammar || '', enableToolResultLimit: !!(s.max_tool_result_tokens), max_tool_result_tokens: s.max_tool_result_tokens || null, reasoning_parser: s.reasoning_parser || '', ttl_seconds: s.ttl_seconds ?? null, enableIndexCache: !!(s.index_cache_freq), index_cache_freq: s.index_cache_freq || null, turboquant_kv_enabled: s.turboquant_kv_enabled || false, turboquant_kv_bits: s.turboquant_kv_bits || 4, specprefill_enabled: s.specprefill_enabled || false, specprefill_draft_model: s.specprefill_draft_model || '', specprefill_keep_pct: s.specprefill_keep_pct ? String(s.specprefill_keep_pct) : '0.2', specprefill_threshold: s.specprefill_threshold || null, dflash_enabled: s.dflash_enabled || false, dflash_draft_model: s.dflash_draft_model || '', dflash_draft_quant_enabled: s.dflash_draft_quant_enabled || false, dflash_draft_quant_weight_bits: s.dflash_draft_quant_weight_bits || 4, dflash_draft_quant_activation_bits: s.dflash_draft_quant_activation_bits || 16, dflash_draft_quant_group_size: s.dflash_draft_quant_group_size || 64, dflash_max_ctx: s.dflash_max_ctx ?? null, dflash_in_memory_cache: s.dflash_in_memory_cache !== false, dflash_in_memory_cache_max_entries: s.dflash_in_memory_cache_max_entries || 4, dflash_in_memory_cache_max_gib: s.dflash_in_memory_cache_max_bytes ? Math.round(s.dflash_in_memory_cache_max_bytes / (1024 ** 3)) : 8, dflash_ssd_cache: s.dflash_ssd_cache || false, dflash_ssd_cache_max_gib: s.dflash_ssd_cache_max_bytes ? Math.round(s.dflash_ssd_cache_max_bytes / (1024 ** 3)) : 20, dflash_draft_window_size: s.dflash_draft_window_size ?? null, dflash_draft_sink_size: s.dflash_draft_sink_size ?? null, dflash_verify_mode: s.dflash_verify_mode || 'adaptive', dflash_compatible: model?.dflash_compatible !== false, dflash_compatibility_reason: model?.dflash_compatibility_reason || '', dflash_ssd_cache_available: !!model?.dflash_ssd_cache_available, mtp_enabled: s.mtp_enabled || false, mtp_compatible: model?.mtp_compatible === true, mtp_compatibility_reason: model?.mtp_compatibility_reason || '', is_paroquant: model?.is_paroquant === true, paroquant_reason: model?.paroquant_reason || '', vlm_mtp_enabled: s.vlm_mtp_enabled || false, vlm_mtp_draft_model: s.vlm_mtp_draft_model || '', vlm_mtp_draft_block_size: s.vlm_mtp_draft_block_size ?? null, ctKwargEntries, is_diffusion_model: isDiffusion, trust_remote_code: s.trust_remote_code || false, }; }, _resetPresetApplicableFields() { // Reset all fields a preset can touch so switching presets does not leave // stale values. Intentionally does NOT touch model_alias / model_type_override // / is_pinned / is_default / turboquant_* / dflash_* / specprefill_* / index_cache_*. const ms = this.modelSettings; ms.temperature = null; ms.top_p = null; ms.top_k = null; ms.min_p = null; ms.repetition_penalty = null; ms.presence_penalty = null; ms.force_sampling = false; ms.max_context_window = null; ms.max_tokens = null; ms.reasoning_parser = null; ms.guided_grammar_enabled = false; ms.guided_grammar = ''; ms.ttl_seconds = null; ms.enable_thinking = null; ms.enableThinkingBudget = false; ms.thinking_budget_tokens = null; ms.enableToolResultLimit = false; ms.max_tool_result_tokens = null; ms.ctKwargEntries = []; }, applyPresetToForm(preset) { // Reset first so previous preset's fields (e.g. presence_penalty) do not stick. this._resetPresetApplicableFields(); const s = preset.settings || {}; const ms = this.modelSettings; const isDiffusion = !!ms.is_diffusion_model; for (const k of Object.keys(s)) { if (isDiffusion && k !== 'chat_template_kwargs' && k !== 'forced_ct_kwargs' && this.isDiffusionUnsupportedProfileField(k)) { continue; } if (k === 'thinking_budget_enabled') { ms.enableThinkingBudget = !!s[k]; } else if (k === 'max_tool_result_tokens') { ms.enableToolResultLimit = s[k] != null; ms.max_tool_result_tokens = s[k] ?? null; } else if (k === 'guided_grammar_enabled') { ms.guided_grammar_enabled = !!s[k]; } else if (k === 'guided_grammar') { ms.guided_grammar = s[k] || ''; } else if (k === 'chat_template_kwargs' || k === 'forced_ct_kwargs') { ms.ctKwargEntries = this.buildCtKwargEntries( s.chat_template_kwargs, s.forced_ct_kwargs, isDiffusion, ); } else { ms[k] = s[k]; } } this.activeProfileName = null; this.profilesDrift = false; }, setScope(scope) { this.profileScope = scope; try { localStorage.setItem('omlx_profile_scope', scope); } catch (e) {} }, isValidProfileName(name) { // Mirror of the backend rule (validate_profile_name) and the // Mac app's isValidSlug. api_name is the exposed model ID // suffix (:), so it must be a clean slug. return /^[a-z0-9][a-z0-9_-]{0,31}$/.test((name || '').trim()); }, slugifyProfileApiName(value) { let slug = (value || '') .normalize('NFKD') .replace(/[\u0300-\u036f]/g, '') .toLowerCase() .replace(/[^a-z0-9_-]+/g, '-') .replace(/-+/g, '-') .replace(/^[-_]+|[-_]+$/g, '') .slice(0, 32) .replace(/[-_]+$/g, ''); return slug || 'profile'; }, async createProfile() { if (!this.selectedModel) return; this.profileError = ''; const displayName = (this.newProfile.display_name || '').trim(); if (!displayName) { this.profileError = 'Name required'; return; } const apiName = (this.newProfile.api_name || this.slugifyProfileApiName(displayName)).trim(); if (!this.isValidProfileName(apiName)) { this.profileError = window.t('modal.model_settings.profiles.invalid_name'); return; } const autoId = 'p-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 6); const body = { name: autoId, display_name: displayName, api_name: apiName, description: (this.newProfile.description || '').trim() || null, settings: this.formValuesForProfile(), also_save_as_template: false, }; try { const r = await fetch( `/admin/api/models/${encodeURIComponent(this.selectedModel.id)}/profiles`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(body) } ); if (r.ok) { await this.loadProfilesForModel(this.selectedModel.id); if (body.also_save_as_template) await this.loadTemplates(); this.showNewProfileForm = false; this.newProfile = { display_name: '', api_name: '', api_name_touched: false, description: '', also_as_template: false }; } else if (r.status === 401) { window.location.href = '/admin'; } else { const data = await r.json().catch(() => ({})); this.profileError = data.detail || 'Failed to save profile'; } } catch (e) { this.profileError = String(e); } }, async applyProfileToForm(profile) { const seq = ++this._applySeq; try { const r = await fetch( `/admin/api/models/${encodeURIComponent(this.selectedModel.id)}/profiles/${encodeURIComponent(profile.name)}/apply`, { method: 'POST' } ); if (seq !== this._applySeq) return; // superseded by a newer click if (r.ok) { const data = await r.json(); const activeName = data.settings?.active_profile_name || profile.name; const settings = { ...(data.settings || {}), active_profile_name: activeName, }; this.modelSettings = this.buildModelSettingsState( this.selectedModel, settings, ); if (this.selectedModel) { this.selectedModel.settings = { ...settings }; } this.activeProfileName = activeName; this.profilesDrift = false; // Update the models list so the profile badge reflects the change const m = this.models.find(m => m.id === this.selectedModel.id); if (m) m.settings = { ...settings }; } else if (r.status === 401) { window.location.href = '/admin'; } } catch (e) { console.error('Failed to apply profile:', e); } }, async applyTemplateToForm(template) { // Check if a profile with this template's name already exists const existingProfile = this.profiles.find(p => p.name === template.name); if (existingProfile) { // Global templates are the source of truth in this scope. const updatedProfile = await this.updateProfile(existingProfile.name, { settings: template.settings, source_template: template.name, }); if (updatedProfile) { await this.applyProfileToForm(updatedProfile); } } else { // Create a new profile from the template const body = { name: template.name, display_name: template.display_name, api_name: this.slugifyProfileApiName(template.display_name || template.name), description: template.description || null, settings: template.settings, source_template: template.name, }; try { const r = await fetch( `/admin/api/models/${encodeURIComponent(this.selectedModel.id)}/profiles`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(body) } ); if (r.ok) { // Reload profiles first to include the new one await this.loadProfilesForModel(this.selectedModel.id); // Find the newly created profile in the refreshed list const newProfile = this.profiles.find(p => p.name === template.name); if (newProfile) { await this.applyProfileToForm(newProfile); } } } catch (e) { console.error('Failed to create profile from template:', e); } } }, async deleteProfile(name) { if (!this.selectedModel) return; try { const r = await fetch( `/admin/api/models/${encodeURIComponent(this.selectedModel.id)}/profiles/${encodeURIComponent(name)}`, { method: 'DELETE' } ); if (r.ok) { if (this.activeProfileName === name) this.activeProfileName = null; await this.loadProfilesForModel(this.selectedModel.id); } else if (r.status === 401) { window.location.href = '/admin'; } } catch (e) { console.error('Delete profile failed:', e); } finally { this.profileDeleteConfirm = null; } }, updateProfileFromEdit(p) { // Edit-dialog save. Internal profile name stays stable; api_name // is the API-visible suffix used by exposed model IDs. this.profileError = ''; const displayName = (p._editDisplayName ?? p.display_name ?? p.name).trim(); const apiName = (p._editApiName ?? p.api_name ?? p.name).trim(); const description = (p._editDescription ?? p.description ?? '').trim(); const exposeAsModel = !!(p._editExposeAsModel ?? p.expose_as_model); if (!displayName) { this.profileError = 'Name required'; return; } if (!this.isValidProfileName(apiName)) { this.profileError = window.t('modal.model_settings.profiles.invalid_name'); return; } const patch = { display_name: displayName, api_name: apiName, description: description, expose_as_model: exposeAsModel, }; return this.updateProfile(p.name, patch); }, updateProfileSettingsFromForm(p) { return this.updateProfile(p.name, { settings: this.formValuesForProfile(), }); }, async updateProfile(name, patch) { // patch: { new_name?, display_name?, api_name?, description?, expose_as_model?, settings?, also_save_as_template? } if (!this.selectedModel) return; this.profileError = ''; try { const r = await fetch( `/admin/api/models/${encodeURIComponent(this.selectedModel.id)}/profiles/${encodeURIComponent(name)}`, { method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(patch) } ); if (r.ok) { const data = await r.json(); if (this.activeProfileName === name && patch.new_name) { this.activeProfileName = patch.new_name; } await this.loadProfilesForModel(this.selectedModel.id); if (patch.also_save_as_template) await this.loadTemplates(); this.editingProfile = null; return data.profile; } else if (r.status === 401) { window.location.href = '/admin'; } else { const data = await r.json().catch(() => ({})); this.profileError = data.detail || 'Failed to update profile'; } } catch (e) { this.profileError = String(e); } }, async createTemplate() { this.profileError = ''; const displayName = this.newTemplate.display_name.trim(); if (!displayName) { this.profileError = 'Name required'; return; } const autoId = 't-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 6); const body = { name: autoId, display_name: displayName, description: this.newTemplate.description.trim() || null, // Only universal fields — server will filter again defensively. settings: this.formValuesForTemplate(), }; try { const r = await fetch('/admin/api/profile-templates', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(body), }); if (r.ok) { await this.loadTemplates(); this.showNewTemplateForm = false; this.newTemplate = { name: '', display_name: '', description: '' }; } else if (r.status === 401) { window.location.href = '/admin'; } else { const data = await r.json().catch(() => ({})); this.profileError = data.detail || 'Failed to save template'; } } catch (e) { this.profileError = String(e); } }, async updateTemplate(name, patch) { this.profileError = ''; try { const r = await fetch( `/admin/api/profile-templates/${encodeURIComponent(name)}`, { method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(patch) } ); if (r.ok) { await this.loadTemplates(); this.editingTemplate = null; } else if (r.status === 401) { window.location.href = '/admin'; } else { const data = await r.json().catch(() => ({})); this.profileError = data.detail || 'Failed to update template'; } } catch (e) { this.profileError = String(e); } }, async deleteTemplate(name) { try { const r = await fetch( `/admin/api/profile-templates/${encodeURIComponent(name)}`, { method: 'DELETE' } ); if (r.ok) { await this.loadTemplates(); } else if (r.status === 401) { window.location.href = '/admin'; } } catch (e) { console.error('Delete template failed:', e); } finally { this.templateDeleteConfirm = null; } }, async openModelSettings(model) { this.profileError = ''; this.showNewProfileForm = false; this.showNewTemplateForm = false; this.editingProfile = null; this.editingTemplate = null; this.profileDeleteConfirm = null; this.templateDeleteConfirm = null; const isDiffusion = this.isDiffusionModel(model); this.activeProfileName = isDiffusion ? null : ((model.settings && model.settings.active_profile_name) || null); if (isDiffusion) { this.profiles = []; this.templates = []; this.profilesDrift = false; } else { try { const saved = localStorage.getItem('omlx_profile_scope'); if (saved === 'preset' || saved === 'global' || saved === 'model') { this.profileScope = saved; } } catch (e) {} await Promise.all([ this.loadProfilesForModel(model.id), this.loadTemplates(), ]); if (this.reasoningParsers.length === 0) { try { const resp = await fetch('/admin/api/grammar/parsers'); if (resp.ok) this.reasoningParsers = await resp.json(); else if (resp.status === 401) window.location.href = '/admin'; } catch (_) { /* network error */ } } } this.selectedModel = model; this.modelSettings = this.buildModelSettingsState( model, model.settings || {}, ); if (isDiffusion) { this.profilesDrift = false; } else { this.computeDrift(); } this.showModelSettingsModal = true; }, async saveModelSettings() { if (!this.selectedModel) return; this.savingModelSettings = true; try { const response = await fetch(`/admin/api/models/${encodeURIComponent(this.selectedModel.id)}/settings`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify((() => { const isDiffusion = !!this.modelSettings.is_diffusion_model; // Build chat_template_kwargs and forced_ct_kwargs from ctKwargEntries const chatTemplateKwargs = {}; const forcedCtKwargs = []; for (const entry of this.modelSettings.ctKwargEntries) { if (entry.type === 'enable_thinking') { if (isDiffusion) continue; chatTemplateKwargs.enable_thinking = entry.value === 'true'; if (entry.force) forcedCtKwargs.push('enable_thinking'); } else if (entry.type === 'reasoning_effort') { if (isDiffusion) continue; chatTemplateKwargs.reasoning_effort = entry.value; if (entry.force) forcedCtKwargs.push('reasoning_effort'); } else if (entry.type === 'custom' && entry.key && entry.key.trim()) { let val = entry.value; if (val === 'true') val = true; else if (val === 'false') val = false; else if (!isNaN(Number(val)) && val.trim() !== '') val = Number(val); const key = entry.key.trim(); if (isDiffusion && this.isDiffusionUnsupportedCtKwarg(key)) { continue; } chatTemplateKwargs[key] = val; if (entry.force) forcedCtKwargs.push(key); } } const payload = { model_alias: this.modelSettings.model_alias?.trim() || null, model_type_override: this.modelSettings.model_type_override || null, max_context_window: this.modelSettings.max_context_window || null, max_tokens: this.modelSettings.max_tokens || null, temperature: Number.isFinite(this.modelSettings.temperature) ? this.modelSettings.temperature : null, top_p: Number.isFinite(this.modelSettings.top_p) ? this.modelSettings.top_p : null, top_k: Number.isFinite(this.modelSettings.top_k) ? this.modelSettings.top_k : null, repetition_penalty: Number.isFinite(this.modelSettings.repetition_penalty) ? this.modelSettings.repetition_penalty : null, min_p: Number.isFinite(this.modelSettings.min_p) ? this.modelSettings.min_p : null, presence_penalty: Number.isFinite(this.modelSettings.presence_penalty) ? this.modelSettings.presence_penalty : null, force_sampling: this.modelSettings.force_sampling, reasoning_parser: this.modelSettings.reasoning_parser || null, ttl_seconds: this.modelSettings.ttl_seconds || null, index_cache_freq: this.modelSettings.enableIndexCache ? (this.modelSettings.index_cache_freq || 4) : 0, enable_thinking: this.modelSettings.enable_thinking, thinking_budget_enabled: this.modelSettings.enableThinkingBudget, thinking_budget_tokens: this.modelSettings.enableThinkingBudget ? (this.modelSettings.thinking_budget_tokens || null) : 0, guided_grammar_enabled: this.modelSettings.guided_grammar_enabled, guided_grammar: this.modelSettings.guided_grammar_enabled ? (this.modelSettings.guided_grammar || null) : null, max_tool_result_tokens: this.modelSettings.enableToolResultLimit ? (this.modelSettings.max_tool_result_tokens || null) : 0, chat_template_kwargs: Object.keys(chatTemplateKwargs).length > 0 ? chatTemplateKwargs : null, forced_ct_kwargs: forcedCtKwargs.length > 0 ? forcedCtKwargs : null, turboquant_kv_enabled: this.modelSettings.turboquant_kv_enabled, turboquant_kv_bits: this.modelSettings.turboquant_kv_enabled ? (parseFloat(this.modelSettings.turboquant_kv_bits) || 4) : 4, specprefill_enabled: this.modelSettings.specprefill_enabled, specprefill_draft_model: this.modelSettings.specprefill_draft_model || null, specprefill_keep_pct: this.modelSettings.specprefill_enabled ? parseFloat(this.modelSettings.specprefill_keep_pct) || 0.2 : null, specprefill_threshold: this.modelSettings.specprefill_enabled ? (this.modelSettings.specprefill_threshold || null) : null, dflash_enabled: this.modelSettings.dflash_enabled, dflash_draft_model: this.modelSettings.dflash_draft_model || null, dflash_draft_quant_enabled: this.modelSettings.dflash_enabled && !!this.modelSettings.dflash_draft_quant_enabled, dflash_draft_quant_weight_bits: this.modelSettings.dflash_enabled && this.modelSettings.dflash_draft_quant_enabled ? parseInt(this.modelSettings.dflash_draft_quant_weight_bits) : null, dflash_draft_quant_activation_bits: this.modelSettings.dflash_enabled && this.modelSettings.dflash_draft_quant_enabled ? parseInt(this.modelSettings.dflash_draft_quant_activation_bits) : null, dflash_draft_quant_group_size: this.modelSettings.dflash_enabled && this.modelSettings.dflash_draft_quant_enabled ? parseInt(this.modelSettings.dflash_draft_quant_group_size) : null, dflash_max_ctx: this.modelSettings.dflash_enabled && this.modelSettings.dflash_max_ctx ? parseInt(this.modelSettings.dflash_max_ctx) : null, dflash_in_memory_cache: this.modelSettings.dflash_enabled ? !!this.modelSettings.dflash_in_memory_cache : true, dflash_in_memory_cache_max_entries: this.modelSettings.dflash_enabled ? (parseInt(this.modelSettings.dflash_in_memory_cache_max_entries) || 4) : 4, dflash_in_memory_cache_max_bytes: this.modelSettings.dflash_enabled ? Math.max(1, parseInt(this.modelSettings.dflash_in_memory_cache_max_gib) || 8) * (1024 ** 3) : 8 * (1024 ** 3), dflash_ssd_cache: this.modelSettings.dflash_enabled && !!this.modelSettings.dflash_in_memory_cache && !!this.modelSettings.dflash_ssd_cache_available && !!this.modelSettings.dflash_ssd_cache, dflash_ssd_cache_max_bytes: this.modelSettings.dflash_enabled ? Math.max(1, parseInt(this.modelSettings.dflash_ssd_cache_max_gib) || 20) * (1024 ** 3) : 20 * (1024 ** 3), // Long-context tuning. Null → server keeps it null → dflash-mlx default. dflash_draft_window_size: this.modelSettings.dflash_enabled && this.modelSettings.dflash_draft_window_size ? parseInt(this.modelSettings.dflash_draft_window_size) : null, dflash_draft_sink_size: this.modelSettings.dflash_enabled && this.modelSettings.dflash_draft_sink_size !== null && this.modelSettings.dflash_draft_sink_size !== undefined && this.modelSettings.dflash_draft_sink_size !== '' ? parseInt(this.modelSettings.dflash_draft_sink_size) : null, dflash_verify_mode: this.modelSettings.dflash_enabled ? (this.modelSettings.dflash_verify_mode || 'adaptive') : null, mtp_enabled: !!this.modelSettings.mtp_enabled, vlm_mtp_enabled: !!this.modelSettings.vlm_mtp_enabled, vlm_mtp_draft_model: this.modelSettings.vlm_mtp_enabled ? (this.modelSettings.vlm_mtp_draft_model || null) : null, vlm_mtp_draft_block_size: this.modelSettings.vlm_mtp_enabled && this.modelSettings.vlm_mtp_draft_block_size ? parseInt(this.modelSettings.vlm_mtp_draft_block_size) : null, trust_remote_code: this.modelSettings.trust_remote_code, }; if (isDiffusion) { Object.assign(payload, { top_p: null, top_k: null, repetition_penalty: null, min_p: null, presence_penalty: null, force_sampling: false, reasoning_parser: null, index_cache_freq: 0, enable_thinking: null, thinking_budget_enabled: false, thinking_budget_tokens: 0, guided_grammar_enabled: false, guided_grammar: null, max_tool_result_tokens: 0, turboquant_kv_enabled: false, turboquant_kv_bits: 4, specprefill_enabled: false, specprefill_draft_model: null, specprefill_keep_pct: null, specprefill_threshold: null, dflash_enabled: false, dflash_draft_model: null, dflash_draft_quant_enabled: false, dflash_draft_quant_weight_bits: null, dflash_draft_quant_activation_bits: null, dflash_draft_quant_group_size: null, dflash_max_ctx: null, dflash_in_memory_cache: true, dflash_in_memory_cache_max_entries: 4, dflash_in_memory_cache_max_bytes: 8 * (1024 ** 3), dflash_ssd_cache: false, dflash_ssd_cache_max_bytes: 20 * (1024 ** 3), dflash_draft_window_size: null, dflash_draft_sink_size: null, dflash_verify_mode: null, mtp_enabled: false, vlm_mtp_enabled: false, vlm_mtp_draft_model: null, vlm_mtp_draft_block_size: null, }); } return payload; })()), }); if (response.ok) { // Refresh the model list to update badges await this.loadModels(); const data = await response.json(); this.showModelSettingsModal = false; if (data.requires_reload) { if (data.auto_reloaded) { alert(window.t('js.info.model_settings_auto_reloaded')); } else if (data.auto_unloaded) { alert(window.t('js.info.model_settings_auto_unloaded')); } else { alert(window.t('js.info.model_type_reload_required')); } } } else if (response.status === 401) { window.location.href = '/admin'; } else { const data = await response.json(); alert(data.detail || window.t('js.error.save_model_settings_failed')); } } catch (err) { console.error('Failed to save model settings:', err); alert(window.t('js.error.save_model_settings_failed')); } finally { this.savingModelSettings = false; } }, async loadGenerationDefaults() { if (!this.selectedModel) return; this.loadingGenDefaults = true; try { const response = await fetch(`/admin/api/models/${encodeURIComponent(this.selectedModel.id)}/generation_config`); if (response.ok) { const data = await response.json(); // Set values from config, clear everything else to Default (null) this.modelSettings.max_context_window = data.max_context_window ?? null; this.modelSettings.temperature = data.temperature ?? null; this.modelSettings.top_p = data.top_p ?? null; this.modelSettings.top_k = data.top_k ?? null; this.modelSettings.repetition_penalty = data.repetition_penalty ?? null; this.modelSettings.max_tokens = null; this.modelSettings.min_p = null; this.modelSettings.presence_penalty = null; this.modelSettings.force_sampling = false; this.modelSettings.reasoning_parser = null; this.modelSettings.guided_grammar_enabled = false; this.modelSettings.guided_grammar = ''; this.modelSettings.ttl_seconds = null; this.modelSettings.enableIndexCache = false; this.modelSettings.index_cache_freq = 0; this.modelSettings.enable_thinking = false; this.modelSettings.enableThinkingBudget = false; this.modelSettings.thinking_budget_tokens = 0; this.modelSettings.enableToolResultLimit = false; this.modelSettings.max_tool_result_tokens = 0; this.modelSettings.ctKwargEntries = []; this.modelSettings.turboquant_kv_enabled = false; this.modelSettings.turboquant_kv_bits = 4; this.modelSettings.specprefill_enabled = false; this.modelSettings.specprefill_draft_model = null; this.modelSettings.specprefill_keep_pct = 0.2; this.modelSettings.specprefill_threshold = null; this.modelSettings.dflash_enabled = false; this.modelSettings.dflash_draft_model = null; this.modelSettings.dflash_draft_quant_enabled = false; this.modelSettings.dflash_draft_quant_weight_bits = null; this.modelSettings.dflash_draft_quant_activation_bits = null; this.modelSettings.dflash_draft_quant_group_size = null; this.modelSettings.dflash_max_ctx = null; this.modelSettings.dflash_in_memory_cache = true; this.modelSettings.dflash_in_memory_cache_max_entries = 4; this.modelSettings.dflash_in_memory_cache_max_gib = 8; this.modelSettings.dflash_ssd_cache = false; this.modelSettings.dflash_ssd_cache_max_gib = 20; this.modelSettings.dflash_draft_window_size = null; this.modelSettings.dflash_draft_sink_size = null; this.modelSettings.dflash_verify_mode = 'adaptive'; this.modelSettings.mtp_enabled = false; this.modelSettings.trust_remote_code = false; } else if (response.status === 404) { alert(window.t('js.error.no_config_defaults')); } else if (response.status === 401) { window.location.href = '/admin'; } else { const data = await response.json(); alert(data.detail || window.t('js.error.load_generation_config_failed')); } } catch (err) { console.error('Failed to load generation config:', err); alert(window.t('js.error.load_generation_config_failed')); } finally { this.loadingGenDefaults = false; } this.activeProfileName = null; this.profilesDrift = false; }, // Status tab functions // Normalizes a host string for safe URL embedding: // - unwraps existing IPv6 brackets so we can re-bracket consistently // - maps unspecified bind addresses (0.0.0.0, ::) to a placeholder // since they are not routable from a client // - maps `localhost` to 127.0.0.1 for consistency with other URLs // - bracket-wraps IPv6 addresses per RFC 3986 (`http://[::1]:8000/v1`) formatDisplayHost(host) { const value = (host || '').trim(); if (!value) return '127.0.0.1'; const unwrapped = value.startsWith('[') && value.endsWith(']') ? value.slice(1, -1) : value; if (unwrapped === '0.0.0.0' || unwrapped === '::') return 'your-ip-address'; if (unwrapped === 'localhost') return '127.0.0.1'; if (unwrapped.includes(':')) return `[${unwrapped}]`; return unwrapped; }, get displayHost() { const host = this.selectedAlias || this.stats.host || '127.0.0.1'; return this.formatDisplayHost(host); }, get ttlPlaceholder() { if (this.selectedModel?.pinned) return window.t('modal.model_settings.ttl_pinned'); const globalTtl = this.globalSettings.idle_timeout?.idle_timeout_seconds; if (globalTtl) { return window.t('modal.model_settings.ttl_global_fallback').replace('{seconds}', globalTtl); } return window.t('modal.model_settings.ttl_no_ttl'); }, async loadServerInfo() { try { const response = await fetch('/admin/api/server-info'); if (response.ok) { const data = await response.json(); const aliases = Array.isArray(data.aliases) ? data.aliases : []; this.serverAliases = aliases; // Preserve user selection across reloads if still valid; // otherwise default to the first alias when available. if (this.selectedAlias && !aliases.includes(this.selectedAlias)) { this.selectedAlias = ''; } if (!this.selectedAlias && aliases.length > 0) { this.selectedAlias = aliases[0]; } } else if (response.status === 401) { window.location.href = '/admin'; } } catch (err) { console.error('Failed to load server info:', err); } }, async restartServerStart() { if (this.restartServer.status === 'restarting' || this.restartServer.status === 'waiting') { return; } if (!window.confirm(window.t('settings.server.restart_confirm'))) { return; } this.restartServer = { status: 'restarting', message: window.t('settings.server.restart_status_sending'), }; let response; try { response = await fetch('/admin/api/server/restart', { method: 'POST' }); } catch (err) { // Network errors mid-restart are expected if the server // dies before sending the 202; fall through to polling. this.restartServer = { status: 'waiting', message: window.t('settings.server.restart_status_waiting'), }; this._restartServerPoll(); return; } if (response.status === 503) { let msg = window.t('settings.server.restart_status_unavailable'); try { const data = await response.json(); if (data && data.detail) msg = data.detail; } catch (e) { /* ignore */ } this.restartServer = { status: 'unsupported', message: msg }; return; } if (response.status === 401) { window.location.href = '/admin'; return; } if (response.status !== 202) { this.restartServer = { status: 'error', message: window.t('settings.server.restart_status_unexpected') .replace('{status}', String(response.status)), }; return; } this.restartServer = { status: 'waiting', message: window.t('settings.server.restart_status_waiting'), }; this._restartServerPoll(); }, _restartServerPoll() { const deadline = Date.now() + 60000; // 60s max wait let sawDownAt = 0; const tick = async () => { if (Date.now() > deadline) { this.restartServer = { status: 'error', message: window.t('settings.server.restart_status_timeout'), }; return; } let alive = false; try { const r = await fetch('/health', { cache: 'no-store' }); alive = r.ok; } catch (e) { alive = false; } if (!alive) { // First time we see it down — record it. We require a // down-then-up transition before declaring success, so // a fast supervisor that hasn't killed the old process // yet doesn't trick us into "instant success". if (!sawDownAt) sawDownAt = Date.now(); setTimeout(tick, 1000); return; } // Alive again. If we never observed the down state, the // restart hasn't actually fired yet — keep polling. if (!sawDownAt) { setTimeout(tick, 1000); return; } this.restartServer = { status: 'idle', message: window.t('settings.server.restart_status_back'), }; // Small delay so the user sees the success state, then // reload to ensure all caches/sessions re-sync. setTimeout(() => window.location.reload(), 500); }; tick(); }, get llmModels() { return this.models.filter(m => m.model_type === 'llm' || m.model_type === 'vlm' || !m.model_type); }, shellQuote(value) { const s = String(value ?? ''); if (!s) return "''"; return `'${s.replace(/'/g, `'"'"'`)}'`; }, shellEnvAssign(name, value) { return `${name}=${this.shellQuote(value)}`; }, get claudeCodeCommand() { const mode = this.globalSettings.claude_code.mode; if (mode === 'cloud') { return 'env -u ANTHROPIC_BASE_URL -u ANTHROPIC_AUTH_TOKEN -u ANTHROPIC_DEFAULT_OPUS_MODEL -u ANTHROPIC_DEFAULT_SONNET_MODEL -u ANTHROPIC_DEFAULT_HAIKU_MODEL -u API_TIMEOUT_MS -u CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC claude'; } // Local mode const port = this.stats.port || 8000; const opusModel = this.globalSettings.claude_code.opus_model || 'select-a-model'; const sonnetModel = this.globalSettings.claude_code.sonnet_model || 'select-a-model'; const haikuModel = this.globalSettings.claude_code.haiku_model || 'select-a-model'; const parts = []; parts.push(this.shellEnvAssign('ANTHROPIC_BASE_URL', `http://${this.displayHost}:${port}`)); if (this.stats.api_key) { parts.push(this.shellEnvAssign('ANTHROPIC_AUTH_TOKEN', this.stats.api_key)); } parts.push(this.shellEnvAssign('ANTHROPIC_DEFAULT_OPUS_MODEL', opusModel)); parts.push(this.shellEnvAssign('ANTHROPIC_DEFAULT_SONNET_MODEL', sonnetModel)); parts.push(this.shellEnvAssign('ANTHROPIC_DEFAULT_HAIKU_MODEL', haikuModel)); parts.push('API_TIMEOUT_MS=3000000'); parts.push('CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1'); parts.push('claude'); return parts.join(' '); }, async saveClaudeCodeSettings() { try { const response = await fetch('/admin/api/global-settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ claude_code_context_scaling_enabled: this.globalSettings.claude_code.context_scaling_enabled, claude_code_target_context_size: this.globalSettings.claude_code.target_context_size, claude_code_mode: this.globalSettings.claude_code.mode, claude_code_opus_model: this.globalSettings.claude_code.opus_model, claude_code_sonnet_model: this.globalSettings.claude_code.sonnet_model, claude_code_haiku_model: this.globalSettings.claude_code.haiku_model, }), }); if (!response.ok) { console.error('Failed to save Claude Code settings'); } } catch (err) { console.error('Failed to save Claude Code settings:', err); } }, _launchCmd(tool) { const raw = this.stats.cli_prefix || 'omlx'; const cli = raw === 'omlx' ? raw : this.shellQuote(raw); return `${cli} launch ${tool}`; }, get claudeCommand() { return this._launchCmd('claude'); }, get codexCommand() { return this._launchCmd('codex'); }, get codexAppCommand() { return this._launchCmd('codex_app'); }, get copilotCommand() { return this._launchCmd('copilot'); }, get opencodeCommand() { return this._launchCmd('opencode'); }, get openclawCommand() { const profile = this.globalSettings.integrations.openclaw_tools_profile || 'coding'; return `${this._launchCmd('openclaw')} --tools-profile ${profile}`; }, get hermesCommand() { return this._launchCmd('hermes'); }, get piCommand() { return this._launchCmd('pi'); }, get markitdownOcrModels() { return (this.models || []).filter((model) => { const configType = String(model.config_model_type || '').toLowerCase(); return configType.includes('ocr'); }); }, async saveIntegrationSettings() { try { const response = await fetch('/admin/api/global-settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ integrations_copilot_model: this.globalSettings.integrations.copilot_model, integrations_codex_model: this.globalSettings.integrations.codex_model, integrations_opencode_model: this.globalSettings.integrations.opencode_model, integrations_openclaw_model: this.globalSettings.integrations.openclaw_model, integrations_hermes_model: this.globalSettings.integrations.hermes_model, integrations_pi_model: this.globalSettings.integrations.pi_model, integrations_openclaw_tools_profile: this.globalSettings.integrations.openclaw_tools_profile, markitdown_enabled: this.globalSettings.integrations.markitdown_enabled, markitdown_expose_model: this.globalSettings.integrations.markitdown_expose_model, markitdown_max_file_size_mb: this.globalSettings.integrations.markitdown_max_file_size_mb, markitdown_max_files_per_request: this.globalSettings.integrations.markitdown_max_files_per_request, markitdown_pdf_processing_engine: this.globalSettings.integrations.markitdown_pdf_processing_engine, }), }); if (!response.ok) { console.error('Failed to save integration settings'); } } catch (err) { console.error('Failed to save integration settings:', err); } }, async saveLanguage(lang) { try { const response = await fetch('/admin/api/global-settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ui_language: lang }) }); if (response.ok) { location.reload(); } else { console.error('Failed to save language'); } } catch (e) { console.error('Failed to save language:', e); } }, async loadStats(includeAlltime = true) { try { const params = new URLSearchParams(); if (this.selectedStatsModel) { params.set('model', this.selectedStatsModel); } const url = '/admin/api/stats' + (params.toString() ? '?' + params : ''); const response = await fetch(url); if (response.ok) { const data = await response.json(); this.stats = { ...this.stats, ...data }; } else if (response.status === 401) { window.location.href = '/admin'; } if (!includeAlltime) { return; } // Load all-time stats const alltimeParams = new URLSearchParams({ scope: 'alltime' }); if (this.selectedStatsModel) { alltimeParams.set('model', this.selectedStatsModel); } const alltimeUrl = '/admin/api/stats?' + alltimeParams; const alltimeResponse = await fetch(alltimeUrl); if (alltimeResponse.ok) { const alltimeData = await alltimeResponse.json(); this.alltimeStats = { ...this.alltimeStats, ...alltimeData }; } } catch (err) { console.error('Failed to load stats:', err); } }, async clearStats() { try { await fetch('/admin/api/stats/clear', { method: 'POST' }); this.showClearStatsConfirm = false; await this.loadStats(); } catch (err) { console.error('Failed to clear stats:', err); this.showClearStatsConfirm = false; } }, async clearAlltimeStats() { try { await fetch('/admin/api/stats/clear-alltime', { method: 'POST' }); this.showClearAlltimeConfirm = false; await this.loadStats(); } catch (err) { console.error('Failed to clear all-time stats:', err); this.showClearAlltimeConfirm = false; } }, async clearSsdCache() { try { const resp = await fetch('/admin/api/ssd-cache/clear', { method: 'POST' }); if (!resp.ok) console.error('SSD cache clear failed:', resp.status); this.showClearSsdCacheConfirm = false; await this.loadStats(); } catch (err) { console.error('Failed to clear SSD cache:', err); this.showClearSsdCacheConfirm = false; } }, async clearHotCache() { try { const resp = await fetch('/admin/api/hot-cache/clear', { method: 'POST' }); if (!resp.ok) console.error('Hot cache clear failed:', resp.status); this.showClearHotCacheConfirm = false; await this.loadStats(); } catch (err) { console.error('Failed to clear hot cache:', err); this.showClearHotCacheConfirm = false; } }, startStatsRefresh() { this.stopStatsRefresh(); this.loadStats(); this._statsRefreshTimer = setInterval(() => { this.loadStats(false); }, 500); }, stopStatsRefresh() { if (this._statsRefreshTimer) { clearInterval(this._statsRefreshTimer); this._statsRefreshTimer = null; } }, formatNumber(num) { if (num >= 1000000000) return (num / 1000000000).toFixed(1) + 'B'; if (num >= 10000000) return (num / 1000000).toFixed(1) + 'M'; return num.toLocaleString(); }, cacheObsCumulative(stats, selectedModel) { const entries = stats.runtime_cache?.models || []; if (entries.length === 0) return {}; if (selectedModel) { const entry = entries.find(m => m.id === selectedModel); return entry?.cache_rates?.cumulative || {}; } const sumKeys = ['prefix_hits', 'prefix_misses', 'evictions', 'ssd_hot_hits', 'ssd_disk_loads', 'ssd_saves', 'hot_cache_evictions', 'hot_cache_promotions']; let agg = {}; for (const m of entries) { const c = m.cache_rates?.cumulative; if (!c || Object.keys(c).length === 0) continue; for (const k of sumKeys) { agg[k] = (agg[k] || 0) + (c[k] || 0); } } const ph = agg.prefix_hits || 0; const pm = agg.prefix_misses || 0; const sh = agg.ssd_hot_hits || 0; const sd = agg.ssd_disk_loads || 0; agg.prefix_hit_rate = (ph + pm) > 0 ? ph / (ph + pm) : 0; agg.ssd_hot_rate = (sh + sd) > 0 ? sh / (sh + sd) : 0; return agg; }, getStatFontClass(value) { if (value >= 1000000000) return 'text-2xl'; if (value >= 1000000) return 'text-3xl'; return 'text-5xl'; }, formatSizeBytes(bytes) { if (bytes >= 1024 * 1024 * 1024) return (bytes / (1024 * 1024 * 1024)).toFixed(1) + ' GB'; if (bytes >= 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(0) + ' MB'; return '0'; }, formatByteCount(bytes) { if (bytes == null || !Number.isFinite(bytes)) return ''; if (bytes >= 1024 * 1024 * 1024) return (bytes / (1024 * 1024 * 1024)).toFixed(1) + ' GB'; if (bytes >= 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; if (bytes >= 1024) return (bytes / 1024).toFixed(1) + ' KB'; return Math.max(0, Math.round(bytes)) + ' B'; }, formatTokenCount(n) { if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M'; if (n >= 1000) return (n / 1000).toFixed(1) + 'k'; return String(n); }, formatDurationShort(seconds) { if (seconds == null || !Number.isFinite(seconds)) return '—'; if (seconds < 1) return seconds.toFixed(1) + 's'; if (seconds < 60) return Math.round(seconds) + 's'; const minutes = Math.floor(seconds / 60); const rem = Math.round(seconds % 60); if (minutes < 60) return minutes + 'm ' + rem + 's'; const hours = Math.floor(minutes / 60); return hours + 'h ' + (minutes % 60) + 'm'; }, formatActivityAge(seconds) { if (seconds == null || !Number.isFinite(seconds)) return ''; return 'last token ' + this.formatDurationShort(seconds) + ' ago'; }, formatActivityMetadata(activity) { const parts = []; if (activity.input_count != null) parts.push(activity.input_count + ' inputs'); if (activity.document_count != null) parts.push(activity.document_count + ' docs'); if (activity.token_count != null) parts.push(this.formatTokenCount(activity.token_count) + ' tok'); if (activity.text_length != null) parts.push(activity.text_length + ' chars'); if (activity.chunk_count != null) parts.push(activity.chunk_count + ' chunks'); if (activity.output_bytes != null) parts.push(this.formatByteCount(activity.output_bytes)); if (activity.file_size_bytes != null && activity.file_size_bytes > 0) parts.push(this.formatByteCount(activity.file_size_bytes)); return parts.join(' · '); }, activityDotClass(seconds) { if (seconds == null || !Number.isFinite(seconds)) return 'bg-green-400 animate-pulse'; if (seconds < 15) return 'bg-green-400 animate-pulse'; if (seconds < 30) return 'bg-amber-400 animate-pulse'; return 'bg-red-400'; }, get runtimeHotCachePercent() { const rc = this.stats.runtime_cache; if (!rc || !rc.hot_cache_max_bytes) return 0; return Math.min(100, (rc.hot_cache_size_bytes / rc.hot_cache_max_bytes) * 100); }, get runtimeSsdCachePercent() { const rc = this.stats.runtime_cache; if (!rc || !rc.disk_max_bytes) return 0; return Math.min(100, (rc.total_size_bytes / rc.disk_max_bytes) * 100); }, get activeModelsPressurePercent() { const mp = this.stats.active_models?.memory_pressure; if (!mp || !mp.hard_bytes) return 0; return Math.min(100, (mp.current_bytes / mp.hard_bytes) * 100); }, get activeModelsSoftPercent() { const mp = this.stats.active_models?.memory_pressure; if (!mp || !mp.hard_bytes || !mp.soft_bytes) return 0; return Math.min(100, (mp.soft_bytes / mp.hard_bytes) * 100); }, get activeModelsPressureBarColor() { const pct = this.activeModelsPressurePercent; if (pct >= 90) return '#ef4444'; if (pct >= 80) return '#f97316'; if (pct >= 70) return '#f59e0b'; if (pct >= 60) return '#facc15'; return '#22c55e'; }, get activeModelsPressureBarStyle() { return `width: ${this.activeModelsPressurePercent}%; height: 100%; display: block; background-color: ${this.activeModelsPressureBarColor};`; }, get activeModelsSoftMarkerStyle() { return `left: ${this.activeModelsSoftPercent}%; width: 1px; background-color: rgba(64, 64, 64, 0.6);`; }, activeModelsPressureLabel() { const mp = this.stats.active_models?.memory_pressure; if (!mp || !mp.enabled || !mp.hard_bytes) { return window.t('status.active_models.enforcer_disabled'); } return `${this.formatSizeBytes(mp.current_bytes)} / ${this.formatSizeBytes(mp.soft_bytes)} soft / ${this.formatSizeBytes(mp.hard_bytes)} hard`; }, modelSizeLabel(model) { if (!model) return '-'; const estimated = model.estimated_size_formatted || '-'; if (model.is_loading) { return estimated; } // actual_size is a rough phys_footprint delta captured at load // time and can include neighboring KV growth — mark with ~obs // so it doesn't read as exact. const actual = model.actual_size_formatted; if (!actual) { return estimated; } if (!estimated || estimated === actual) { return `~${actual} obs`; } return `~${actual} obs / ${estimated} est`; }, copyToClipboard(text) { if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(text).catch(() => { this._copyFallback(text); }); } else { this._copyFallback(text); } }, _copyFallback(text) { const textarea = document.createElement('textarea'); textarea.value = text; textarea.style.position = 'fixed'; textarea.style.opacity = '0'; document.body.appendChild(textarea); textarea.select(); try { document.execCommand('copy'); } catch (err) { console.error('Failed to copy:', err); } document.body.removeChild(textarea); }, async logout() { try { await fetch('/admin/api/logout', { method: 'POST' }); } catch (err) { console.error('Logout error:', err); } finally { window.location.href = '/admin'; } }, // Shared external endpoint settings (both bench tabs) saveExternalEndpoint() { localStorage.setItem('omlx_bench_external_base_url', this.externalBaseUrl.trim()); localStorage.setItem('omlx_bench_external_api_key', this.externalApiKey); localStorage.setItem('omlx_bench_external_model', this.externalModel.trim()); }, externalConfigValid() { return !!(this.externalBaseUrl.trim() && this.externalModel.trim()); }, externalRequestBody() { return { base_url: this.externalBaseUrl.trim(), api_key: this.externalApiKey, model: this.externalModel.trim(), }; }, // Benchmark functions async startBenchmark() { if (this.benchExternalEnabled) { if (!this.externalConfigValid()) { this.benchError = window.t('js.error.external_endpoint_required'); return; } } else if (!this.benchModelId) { return; } // Collect selected prompt lengths const promptLengths = Object.entries(this.benchPromptLengths) .filter(([_, v]) => v) .map(([k, _]) => parseInt(k)); if (promptLengths.length === 0) { this.benchError = window.t('js.error.select_prompt_length'); return; } // Collect selected batch sizes const batchSizes = Object.entries(this.benchBatchSizes) .filter(([_, v]) => v) .map(([k, _]) => parseInt(k)); // Load device info if not loaded yet if (!this.benchDeviceInfo) { this.loadBenchDeviceInfo(); } // Reset state this.benchRunning = true; this.benchProgress = null; this.benchSingleResults = []; this.benchBatchResults = []; this.benchError = ''; this.benchBenchId = null; this.benchUploadResults = []; this.benchUploadDone = null; this.benchUploading = false; this.benchUploadSkipped = null; this.benchRunExternal = this.benchExternalEnabled ? { base_url: this.externalBaseUrl.trim(), model: this.externalModel.trim() } : null; try { const response = await fetch('/admin/api/bench/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model_id: this.benchExternalEnabled ? this.externalModel.trim() : this.benchModelId, prompt_lengths: promptLengths, generation_length: 128, batch_sizes: batchSizes, force_lm_engine: this.benchExternalEnabled ? false : this.benchForceLmEngine, external: this.benchExternalEnabled ? this.externalRequestBody() : null, }), }); if (response.status === 401) { window.location.href = '/admin'; return; } if (!response.ok) { const data = await response.json(); this.benchError = data.detail || window.t('js.error.start_benchmark_failed'); this.benchRunning = false; return; } const data = await response.json(); this.benchBenchId = data.bench_id; this.connectBenchSSE(data.bench_id); } catch (err) { console.error('Failed to start benchmark:', err); this.benchError = window.t('js.error.start_benchmark_error').replace('{message}', err.message); this.benchRunning = false; } }, connectBenchSSE(benchId) { if (this.benchEventSource) { this.benchEventSource.close(); } const es = new EventSource(`/admin/api/bench/${benchId}/stream`); this.benchEventSource = es; es.onmessage = (event) => { try { const data = JSON.parse(event.data); if (data.type === 'progress') { this.benchProgress = { phase: data.phase, message: data.message, current: data.current, total: data.total, }; } else if (data.type === 'result') { // SSE replay-on-subscribe re-delivers every event on // every reconnect (incl. page refresh), so append-only // arrays must dedupe. Single rows are keyed by // (pp, tg); batch rows by batch_size. if (data.data.test_type === 'single') { const exists = this.benchSingleResults.some( r => r.pp === data.data.pp && r.tg === data.data.tg ); if (!exists) { this.benchSingleResults = [...this.benchSingleResults, data.data]; } } else if (data.data.test_type === 'batch') { const exists = this.benchBatchResults.some( r => r.batch_size === data.data.batch_size ); if (!exists) { this.benchBatchResults = [...this.benchBatchResults, data.data]; } } } else if (data.type === 'done') { // Benchmark tests done, uploading starts this.benchUploading = true; this.benchProgress = { phase: 'upload', message: 'Uploading to community benchmarks...', current: 0, total: 0, }; this.loadModels(); } else if (data.type === 'upload') { // Dedupe on replay: upload entries are unique by context_length. const exists = this.benchUploadResults.some( r => r.context_length === data.data.context_length ); if (!exists) { this.benchUploadResults = [...this.benchUploadResults, data.data]; } } else if (data.type === 'upload_done') { this.benchUploadDone = data.data; this.benchUploading = false; this.benchRunning = false; this.benchProgress = null; es.close(); this.benchEventSource = null; } else if (data.type === 'upload_skipped') { this.benchUploadSkipped = { reason: data.reason || 'experimental_features', features: data.features || [], }; this.benchUploading = false; this.benchRunning = false; this.benchProgress = null; es.close(); this.benchEventSource = null; this.loadModels(); } else if (data.type === 'error') { this.benchError = data.message; this.benchRunning = false; this.benchProgress = null; es.close(); this.benchEventSource = null; this.loadModels(); } } catch (err) { console.error('Failed to parse SSE event:', err); } }; es.onerror = () => { if (this.benchRunning) { this.benchError = window.t('js.error.benchmark_connection_lost'); this.benchRunning = false; this.benchProgress = null; } es.close(); this.benchEventSource = null; }; }, async cancelBenchmark() { if (!this.benchBenchId) return; try { await fetch(`/admin/api/bench/${this.benchBenchId}/cancel`, { method: 'POST' }); } catch (err) { console.error('Failed to cancel benchmark:', err); } // SSE handler will update state when error/done event arrives }, benchGetSpeedup(batchResult) { const baseline = this.benchSingleResults.find(r => r.pp === 1024); if (!baseline || !baseline.gen_tps || baseline.gen_tps <= 0) return 0; return batchResult.tg_tps / baseline.gen_tps; }, benchFormatMemory(bytes) { if (!bytes || bytes === 0) return '-'; const gb = bytes / (1024 * 1024 * 1024); if (gb >= 1) return gb.toFixed(2) + ' GB'; const mb = bytes / (1024 * 1024); return mb.toFixed(0) + ' MB'; }, benchBuildText() { const pad = (s, w) => s.toString().padStart(w); const rpad = (s, w) => s.toString().padEnd(w); let lines = []; lines.push('oMLX - LLM inference, optimized for your Mac'); lines.push('https://github.com/jundot/omlx'); if (this.benchRunExternal) { lines.push(`Benchmark Model: ${this.benchRunExternal.model} @ ${this.benchRunExternal.base_url}`); lines.push('Engine: External OpenAI-compatible endpoint'); } else { lines.push(`Benchmark Model: ${this.benchModelId}`); lines.push(`Engine: ${this.benchForceLmEngine ? 'Force mlx-lm' : 'Auto'}`); } lines.push('='.repeat(80)); // Single Request Results if (this.benchSingleResults.length > 0) { lines.push(''); lines.push('Single Request Results'); lines.push('-'.repeat(80)); const hdr = [rpad('Test', 16), pad('TTFT(ms)', 10), pad('TPOT(ms)', 10), pad('pp TPS', 12), pad('tg TPS', 12), pad('E2E(s)', 10), pad('Throughput', 12), pad('Peak Mem', 10)]; lines.push(hdr.join(' ')); for (const r of this.benchSingleResults) { const row = [ rpad(`pp${r.pp}/tg${r.tg}`, 16), pad(r.ttft_ms.toFixed(1), 10), pad(r.tpot_ms.toFixed(2), 10), pad(r.processing_tps.toFixed(1) + ' tok/s', 12), pad(r.gen_tps.toFixed(1) + ' tok/s', 12), pad(r.e2e_latency_s.toFixed(3), 10), pad(r.total_throughput.toFixed(1) + ' tok/s', 12), pad(this.benchFormatMemory(r.peak_memory_bytes), 10), ]; lines.push(row.join(' ')); } } // Helper for batch table text const buildBatchText = (title, subtitle, results) => { if (results.length === 0) return; const baseline = this.benchSingleResults.find(r => r.pp === 1024); lines.push(''); lines.push(`${title}`); lines.push(subtitle); lines.push('-'.repeat(80)); const hdr = [rpad('Batch', 8), pad('tg TPS', 12), pad('Speedup', 8), pad('pp TPS', 12), pad('pp TPS/req', 12), pad('TTFT(ms)', 10), pad('E2E(s)', 10)]; lines.push(hdr.join(' ')); if (baseline) { const row = [ rpad('1x', 8), pad(baseline.gen_tps.toFixed(1) + ' tok/s', 12), pad('1.00x', 8), pad(baseline.processing_tps.toFixed(1) + ' tok/s', 12), pad(baseline.processing_tps.toFixed(1) + ' tok/s', 12), pad(baseline.ttft_ms.toFixed(1), 10), pad(baseline.e2e_latency_s.toFixed(3), 10), ]; lines.push(row.join(' ')); } for (const r of results) { const speedup = baseline && baseline.gen_tps > 0 ? (r.tg_tps / baseline.gen_tps).toFixed(2) + 'x' : '-'; const row = [ rpad(r.batch_size + 'x', 8), pad(r.tg_tps.toFixed(1) + ' tok/s', 12), pad(speedup, 8), pad(r.pp_tps.toFixed(1) + ' tok/s', 12), pad((r.pp_tps / r.batch_size).toFixed(1) + ' tok/s', 12), pad(r.avg_ttft_ms.toFixed(1), 10), pad(r.e2e_latency_s.toFixed(3), 10), ]; lines.push(row.join(' ')); } }; buildBatchText( 'Continuous Batching', 'pp1024 / tg128', this.benchBatchResults ); return lines.join('\n'); }, benchCopyText() { const text = this.benchBuildText(); const onSuccess = () => { this.benchCopied = true; setTimeout(() => { this.benchCopied = false; }, 2000); }; if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(text).then(onSuccess).catch(() => { this._copyFallback(text); onSuccess(); }); } else { this._copyFallback(text); onSuccess(); } }, async loadBenchDeviceInfo() { try { const resp = await fetch('/admin/api/device-info'); if (resp.ok) { this.benchDeviceInfo = await resp.json(); } } catch (err) { console.error('Failed to load device info:', err); } }, async loadBenchState() { // Discover an in-progress throughput run on tab/page load so // a second tab (or a refresh) can attach to its SSE stream // and replay the run's full event history. // // Three cases: // 1. No active run → clear any stale banner state. // 2. Active run, this tab is already attached → no-op. // 3. Active run, this tab is fresh → auto-attach. // 4. Active run, this tab is displaying a *different* // completed bench → show banner; let the user decide // whether to clobber their result view. try { const resp = await fetch('/admin/api/bench/active'); if (!resp.ok) return; const data = await resp.json(); if (!data.running || !data.bench_id) { this.benchOtherActive = null; return; } // Already attached to this bench — nothing to do. if (this.benchBenchId === data.bench_id && this.benchEventSource) { return; } // We have completed results from a DIFFERENT bench on // screen — don't silently swap them out. Show a banner // so the user can explicitly accept the new bench. const hasStaleResults = !this.benchRunning && this.benchBenchId && this.benchBenchId !== data.bench_id && (this.benchSingleResults.length > 0 || this.benchBatchResults.length > 0); if (hasStaleResults) { this.benchOtherActive = { bench_id: data.bench_id, model_id: data.model_id, force_lm_engine: !!data.force_lm_engine, external: !!data.external, }; return; } // Fresh slate: attach. this.benchBenchId = data.bench_id; this._restoreBenchRunSource(data); this.benchRunning = true; this.benchOtherActive = null; this.connectBenchSSE(data.bench_id); } catch (err) { console.error('Failed to load bench state:', err); } }, // Restore the config UI from an active run discovered via // /api/bench/active. External model ids aren't in the local // dropdown, so the external flag drives which controls light up. _restoreBenchRunSource(data) { if (data.external) { this.benchExternalEnabled = true; this.benchRunExternal = { // base_url is intentionally not exposed by the API; // fall back to this browser's stored setting. base_url: this.externalBaseUrl.trim(), model: data.model_id, }; } else { this.benchModelId = data.model_id; this.benchForceLmEngine = !!data.force_lm_engine; this.benchExternalEnabled = false; this.benchRunExternal = null; } }, // User clicked "View live" on the banner — clear the stale // result display, attach to the active run. The replay-on- // subscribe stream re-delivers every event so the new bench // populates its table from the start. acceptOtherBench() { if (!this.benchOtherActive) return; const other = this.benchOtherActive; this.benchOtherActive = null; this.benchBenchId = other.bench_id; this._restoreBenchRunSource(other); this.benchRunning = true; this.benchSingleResults = []; this.benchBatchResults = []; this.benchUploadResults = []; this.benchUploadDone = null; this.benchUploadSkipped = null; this.benchProgress = null; this.benchError = ''; this.connectBenchSSE(other.bench_id); }, dismissOtherBench() { // Hide for now; the banner reappears next loadBenchState if // the run is still active. Use case: user wants to keep // reviewing their previous result for a moment. this.benchOtherActive = null; }, // Bench sub-tab setBenchTab(tab) { if (!DASHBOARD_BENCH_TABS.has(tab)) return; this.benchTab = tab; this.mainTab = 'bench'; this.syncTabStateToUrl(); if (tab === 'throughput') { this.loadBenchDeviceInfo(); this.loadBenchState(); } }, // Accuracy benchmark functions async loadAccState() { // Load accumulated results + queue status from server (page load / tab switch) try { const resp = await fetch('/admin/api/bench/accuracy/results'); if (resp.ok) { const data = await resp.json(); this.accAllResults = (data.results || []).map(r => ({ ...r, _showCategories: false })); this.accRunning = data.running || false; this.accCurrentModel = data.current_model || ''; if (data.current_bench_id && data.running) { this.accCurrentBenchId = data.current_bench_id; this.connectAccSSE(data.current_bench_id); } } } catch (err) { console.error('Failed to load accuracy state:', err); } await this.loadAccQueueStatus(); }, async loadAccQueueStatus() { try { const resp = await fetch('/admin/api/bench/accuracy/queue/status'); if (resp.ok) { const data = await resp.json(); this.accQueue = data.queue || []; this.accRunning = data.running || false; this.accCurrentModel = data.current_model || ''; if (data.current_bench_id) { this.accCurrentBenchId = data.current_bench_id; } // Restore last progress for reconnect if (data.last_progress && data.running) { this.accProgress = data.last_progress; } } } catch (err) { console.error('Failed to load queue status:', err); } }, async addToAccQueue() { if (this.accExternalEnabled) { if (!this.externalConfigValid()) { this.accError = window.t('js.error.external_endpoint_required'); return; } } else if (!this.accModelId) { return; } const selected = Object.entries(this.accBenchmarks) .filter(([_, v]) => v) .map(([k]) => k); if (selected.length === 0) return; this.accError = ''; try { const resp = await fetch('/admin/api/bench/accuracy/queue/add', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model_id: this.accExternalEnabled ? this.externalModel.trim() : this.accModelId, benchmarks: Object.fromEntries( selected.map(k => [k, this.accSampleSizes[k]]) ), batch_size: this.accBatchSize, enable_thinking: this.accExternalEnabled ? false : this.accEnableThinking, sampling_profile: this.accSamplingProfile, external: this.accExternalEnabled ? this.externalRequestBody() : null, }), }); if (!resp.ok) { const err = await resp.json(); throw new Error(err.detail || 'Failed to add to queue'); } const data = await resp.json(); this.accQueue = data.queue || []; this.accRunning = data.running || false; this.accCurrentModel = data.current_model || ''; if (data.last_progress) this.accProgress = data.last_progress; // Connect SSE to current run if (data.current_bench_id) { this.accCurrentBenchId = data.current_bench_id; this.connectAccSSE(data.current_bench_id); } } catch (err) { this.accError = err.message; } }, async removeFromAccQueue(idx) { try { await fetch(`/admin/api/bench/accuracy/queue/${idx}`, { method: 'DELETE' }); await this.loadAccQueueStatus(); } catch (err) { console.error('Failed to remove from queue:', err); } }, connectAccSSE(benchId) { if (this.accEventSource) { this.accEventSource.close(); } this._stopAccPolling(); const es = new EventSource(`/admin/api/bench/accuracy/${benchId}/stream`); this.accEventSource = es; es.onmessage = (event) => { try { const data = JSON.parse(event.data); switch (data.type) { case 'progress': this.accProgress = data; this.accCurrentModel = data.model_id || this.accCurrentModel; break; case 'result': // Dedupe on replay: accuracy results are unique by // (model_id, benchmark). { const exists = this.accAllResults.some( r => r.model_id === data.data.model_id && r.benchmark === data.data.benchmark ); if (!exists) { data.data._showCategories = false; this.accAllResults.push(data.data); } } break; case 'done': this.accProgress = null; es.close(); this.accEventSource = null; // Check for next in queue this._pollForNextRun(); break; case 'error': this.accError = data.message; this.accProgress = null; es.close(); this.accEventSource = null; this.loadAccQueueStatus(); break; } } catch (err) { console.error('SSE parse error:', err); } }; es.onerror = () => { es.close(); this.accEventSource = null; // SSE disconnected — fall back to polling this._startAccPolling(); }; }, _startAccPolling() { this._stopAccPolling(); this._accPollTimer = setInterval(async () => { await this.loadAccQueueStatus(); // Load latest results too try { const resp = await fetch('/admin/api/bench/accuracy/results'); if (resp.ok) { const data = await resp.json(); this.accAllResults = (data.results || []).map(r => ({ ...r, _showCategories: false })); } } catch (e) {} // Try to reconnect SSE if running if (this.accRunning && this.accCurrentBenchId && !this.accEventSource) { this._stopAccPolling(); this.connectAccSSE(this.accCurrentBenchId); } if (!this.accRunning) { this._stopAccPolling(); } }, 3000); }, _stopAccPolling() { if (this._accPollTimer) { clearInterval(this._accPollTimer); this._accPollTimer = null; } }, _pollForNextRun() { // After a run completes, poll briefly for the next run to start let attempts = 0; const poll = setInterval(async () => { attempts++; await this.loadAccQueueStatus(); if (this.accCurrentBenchId && this.accRunning) { clearInterval(poll); this.connectAccSSE(this.accCurrentBenchId); } else if (!this.accRunning || attempts > 10) { clearInterval(poll); } }, 1000); }, async cancelAccuracyBenchmark() { try { await fetch('/admin/api/bench/accuracy/cancel', { method: 'POST' }); } catch (err) { console.error('Cancel error:', err); } this.accRunning = false; this.accProgress = null; this.accQueue = []; this.accCurrentModel = ''; if (this.accEventSource) { this.accEventSource.close(); this.accEventSource = null; } }, async resetAccResults() { try { await fetch('/admin/api/bench/accuracy/results/reset', { method: 'POST' }); this.accAllResults = []; } catch (err) { console.error('Reset error:', err); } }, accBuildText() { if (this.accAllResults.length === 0) return ''; const pad = (s, w) => s.toString().padStart(w); const rpad = (s, w) => s.toString().padEnd(w); // Group by model const models = [...new Set(this.accAllResults.map(r => r.model_id))]; const benchmarks = [...new Set(this.accAllResults.map(r => r.benchmark))]; // Build lookup: model -> benchmark -> accuracy const lookup = {}; for (const r of this.accAllResults) { if (!lookup[r.model_id]) lookup[r.model_id] = {}; lookup[r.model_id][r.benchmark] = r; } // Full sizes lookup const fullSizes = {}; for (const grp of this.accBenchmarkGroups) { for (const bl of grp.benchmarks) fullSizes[bl.key] = bl.fullSize; } // Determine column widths const modelWidth = Math.max(12, ...models.map(m => m.length + 2)); const modeW = 8; const sampledW = 14; const benchWidth = Math.max(14, ...benchmarks.map(b => b.length + 2)); let lines = []; lines.push('Intelligence Benchmark Comparison'); lines.push(''); // Header row let header = rpad('', benchWidth) + rpad('Mode', modeW) + rpad('Sampled', sampledW); for (const m of models) header += pad(m, modelWidth); lines.push(header); lines.push('-'.repeat(benchWidth + modeW + sampledW + models.length * modelWidth)); // Data rows for (const b of benchmarks) { // Get sample info from first available result for this benchmark const sample = models.map(m => lookup[m]?.[b]).find(r => r); const total = sample?.total || 0; const full = fullSizes[b] || 0; const isFull = total >= full; const mode = isFull ? 'Full' : 'Sample'; const sampledStr = isFull ? String(full) : (total + '/' + full); let row = rpad(b.toUpperCase(), benchWidth) + rpad(mode, modeW) + rpad(sampledStr, sampledW); for (const m of models) { const r = lookup[m]?.[b]; row += pad(r ? (r.accuracy * 100).toFixed(1) + '%' : '-', modelWidth); } lines.push(row); } // Detail section per model lines.push(''); lines.push('--- Detail ---'); for (const m of models) { lines.push(''); lines.push('Model: ' + m); lines.push(rpad('Benchmark', 16) + pad('Accuracy', 10) + pad('Correct', 10) + pad('Total', 8) + pad('Time(s)', 10) + pad('Think', 8)); lines.push('-'.repeat(62)); for (const r of this.accAllResults.filter(r => r.model_id === m)) { lines.push( rpad(r.benchmark.toUpperCase(), 16) + pad((r.accuracy * 100).toFixed(1) + '%', 10) + pad(r.correct, 10) + pad(r.total, 8) + pad(r.time_s, 10) + pad(r.thinking_used ? 'Yes' : 'No', 8) ); } } return lines.join('\n'); }, accCopyText() { const text = this.accBuildText(); const onSuccess = () => { this.accCopied = true; setTimeout(() => { this.accCopied = false; }, 2000); }; if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(onSuccess).catch(() => { const ta = document.getElementById('accTextarea'); if (ta) { ta.select(); document.execCommand('copy'); onSuccess(); } }); } else { const ta = document.getElementById('accTextarea'); if (ta) { ta.select(); document.execCommand('copy'); onSuccess(); } } }, accDownloadResult(r, format) { const filename = `${r.model_id}_${r.benchmark}.${format}`; let content, mime; const qr = r.question_results || []; if (format === 'json') { content = JSON.stringify({ model_id: r.model_id, benchmark: r.benchmark, accuracy: r.accuracy, correct: r.correct, total: r.total, time_s: r.time_s, thinking_used: r.thinking_used || false, category_scores: r.category_scores || null, questions: qr, }, null, 2); mime = 'application/json'; } else if (format === 'csv') { const esc = s => '"' + (s || '').replace(/"/g, '""') + '"'; const lines = ['id,category,correct,expected,predicted,question,raw_response,time_s']; for (const q of qr) { lines.push([q.id, esc(q.category || ''), q.correct, esc(q.expected), esc(q.predicted), esc(q.question), esc(q.raw_response), q.time_s].join(',')); } content = lines.join('\n'); mime = 'text/csv'; } else { const lines = [ `Model: ${r.model_id}`, `Benchmark: ${r.benchmark.toUpperCase()}`, `Accuracy: ${(r.accuracy * 100).toFixed(1)}% (${r.correct}/${r.total})`, `Time: ${r.time_s}s`, '', ]; for (const q of qr) { lines.push(`--- Q${q.id} [${q.correct ? 'CORRECT' : 'WRONG'}] ---`); if (q.category) lines.push(`Category: ${q.category}`); lines.push(`Question: ${q.question || ''}`); lines.push(`Expected: ${q.expected}`); lines.push(`Predicted: ${q.predicted}`); lines.push(`Raw response: ${q.raw_response || '(empty)'}`); lines.push(`Time: ${q.time_s}s`); lines.push(''); } content = lines.join('\n'); mime = 'text/plain'; } const blob = new Blob([content], { type: mime }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); }, // Log viewer functions filteredLogContent() { const LEVELS = ['TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']; const minIdx = LEVELS.indexOf(this.logMinLevel); if (minIdx <= 0) return this.logContent; const levelRe = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3} - \S+ - (TRACE|DEBUG|INFO|WARNING|ERROR|CRITICAL) - /; let visible = true; return this.logContent.split('\n').filter(line => { const m = line.match(levelRe); if (m) visible = LEVELS.indexOf(m[1]) >= minIdx; return visible; }).join('\n'); }, levelButtonClass(lvl) { const LEVELS = ['TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']; const idx = LEVELS.indexOf(lvl); const minIdx = LEVELS.indexOf(this.logMinLevel); if (idx < minIdx) return 'bg-neutral-100 text-neutral-300'; if (idx === minIdx) return 'bg-neutral-900 text-white'; return 'bg-neutral-200 text-neutral-700'; }, async loadLogs() { this.logLoading = true; this.logError = ''; try { const params = new URLSearchParams({ lines: this.logLines.toString(), }); if (this.logFile && this.logFile !== 'server.log') { params.append('file', this.logFile); } const response = await fetch(`/admin/api/logs?${params}`); if (response.ok) { const data = await response.json(); this.logContent = data.logs; this.logTotalLines = data.total_lines; this.logAvailableFiles = data.available_files || ['server.log']; this.logLastUpdated = new Date().toLocaleTimeString(); // Auto-scroll to bottom if (this.logAutoScroll) { this.$nextTick(() => { const textarea = this.$refs.logTextarea; if (textarea) { textarea.scrollTop = textarea.scrollHeight; } }); } } else if (response.status === 401) { window.location.href = '/admin'; } else { const data = await response.json(); this.logError = data.detail || window.t('js.error.load_logs_failed'); } } catch (err) { console.error('Failed to load logs:', err); this.logError = window.t('js.error.load_logs_failed'); } finally { this.logLoading = false; } }, startLogRefresh() { this.stopLogRefresh(); // Clear existing timer if (this.logRefreshInterval > 0) { this.logAutoRefresh = true; this._logRefreshTimer = setInterval(() => { this.loadLogs(); }, this.logRefreshInterval * 1000); } }, stopLogRefresh() { if (this._logRefreshTimer) { clearInterval(this._logRefreshTimer); this._logRefreshTimer = null; } this.logAutoRefresh = false; }, restartLogRefresh() { if (this.mainTab === 'logs') { this.startLogRefresh(); } }, // Parse cache size string (e.g., "10GB") to percent of SSD total capacity parseCacheToPercent(cacheStr, totalBytes) { if (!cacheStr || cacheStr === 'auto' || !totalBytes || totalBytes === 0) { return 10; // Default 10% } const match = cacheStr.match(/^(\d+(?:\.\d+)?)\s*(GB|MB|TB)?$/i); if (!match) return 10; let bytes = parseFloat(match[1]); const unit = (match[2] || 'GB').toUpperCase(); if (unit === 'TB') bytes *= 1024 * 1024 * 1024 * 1024; else if (unit === 'GB') bytes *= 1024 * 1024 * 1024; else if (unit === 'MB') bytes *= 1024 * 1024; const percent = Math.round((bytes / totalBytes) * 100); return Math.min(100, percent); }, // Convert percent to cache size string percentToCacheString(percent, totalBytes) { if (!totalBytes || totalBytes === 0) return 'auto'; const bytes = Math.floor((percent / 100) * totalBytes); const gb = Math.floor(bytes / (1024 * 1024 * 1024)); return `${gb}GB`; }, // Helper: parse GB from a settings string like "68GB", "1TB", "512MB" _parseSettingsGB(val) { if (!val) return null; const match = val.match(/^(\d+(?:\.\d+)?)\s*(GB|MB|TB)?$/i); if (!match) return null; let num = parseFloat(match[1]); const unit = (match[2] || 'GB').toUpperCase(); if (unit === 'TB') return Math.round(num * 1024); if (unit === 'MB') return Math.round(num / 1024); return Math.round(num); }, // Memory guard tier → live hard ceiling (GB) for the selected tier. // Mirrors ProcessMemoryEnforcer._get_hard_limit_bytes: // static_ceiling = total - tier.static_reserve // dynamic_ceiling = omlx_phys + free + inactive + active * ratio // final = min(static, dynamic, metal_cap) // The static / dynamic inputs come from the global-settings // response and reflect the moment that response was fetched. // Warning shown below the breakdown when the kernel // iogpu.wired_limit_mb is lower than what oMLX asked Metal // to allow at start. Returns an HTML string with the exact // sysctl command the user can paste into Terminal, or "" when // the kernel cap is fine. // True when the kernel iogpu.wired_limit_mb (or Apple default // working set) caps oMLX below its desired static ceiling. get memoryGuardShowWiredLimitWarning() { const sys = this.globalSettings.system || {}; const kernelBytes = sys.iogpu_wired_limit_bytes || 0; const requestedBytes = sys.omlx_wired_limit_request_bytes || 0; if (kernelBytes <= 0 || requestedBytes <= 0) return false; return kernelBytes < requestedBytes; }, // Red bold warning text (no copy button). The button is a // separate sibling in the template so Alpine can wire @click. get memoryGuardWiredLimitWarningHTML() { if (!this.memoryGuardShowWiredLimitWarning) return ''; const sys = this.globalSettings.system; const kernelGB = (sys.iogpu_wired_limit_bytes / (1024 ** 3)).toFixed(1); const template = window.t('settings.resource.guard_tier.wired_limit_warning'); return template.replaceAll( '{kernel}', `${kernelGB} GB`, ); }, // The sysctl command rendered in the dark bold chip. get memoryGuardWiredLimitCommand() { if (!this.memoryGuardShowWiredLimitWarning) return ''; const requested = this.globalSettings.system.omlx_wired_limit_request_bytes; const requestedMB = Math.ceil(requested / (1024 ** 2)); return `sudo sysctl iogpu.wired_limit_mb=${requestedMB}`; }, // 2-second "Copied!" affordance after the clipboard button is // pressed. Reset by the same setTimeout so it's harmless if // the user clicks rapidly. wiredLimitCopied: false, copyWiredLimitCommand() { const text = this.memoryGuardWiredLimitCommand; if (!text) return; const onSuccess = () => { this.wiredLimitCopied = true; setTimeout(() => { this.wiredLimitCopied = false; }, 2000); }; if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(onSuccess).catch(() => { onSuccess(); }); } else { onSuccess(); } }, // Description text shown next to the Memory guard tier dropdown. // safe / balanced / aggressive get a "free + inactive + N% of // active (via macOS reclaim_method)" sentence. custom shows the // user-supplied ceiling. get memoryGuardTierDescription() { const tier = this.globalSettings.memory?.memory_guard_tier || 'balanced'; const tierLabel = window.t('settings.resource.guard_tier.' + tier); if (tier === 'custom') { const gb = Number( this.globalSettings.memory?.memory_guard_custom_ceiling_gb || 0 ).toFixed(1); return window .t('settings.resource.guard_tier.description_custom') .replace('{custom_gb}', gb); } const pct = { safe: 20, balanced: 50, aggressive: 80 }[tier] ?? 50; const method = window.t( 'settings.resource.guard_tier.reclaim_method.' + tier ); return window .t('settings.resource.guard_tier.description_template') .replace('{tier}', tierLabel) .replace('{active_pct}', pct) .replace('{reclaim_method}', method); }, // Breakdown line. For ratio tiers: `Free X, inactive Y, active Z // × N% = R → ceiling C`. For custom: `Custom ceiling X GB → // effective ceiling C` (after clamp by static / metal cap). get memoryGuardBreakdownHTML() { const sys = this.globalSettings.system || {}; const GB = 1024 ** 3; const tier = this.globalSettings.memory?.memory_guard_tier || 'balanced'; const fmt = (gb) => Number(gb).toFixed(1); const bold = (gb) => `${fmt(gb)} GB`; // Static / metal cap for the final clamp shown to the user. const totalGB = (sys.total_memory_bytes || 0) / GB; const staticReserveGB = tier === 'custom' ? 2 : totalGB < 16 ? 4 : { safe: 8, balanced: 6, aggressive: 4 }[tier] ?? 6; const staticCeiling = Math.max(0, totalGB - staticReserveGB); const metalCapGB = (sys.iogpu_wired_limit_bytes || 0) / GB; // Helper: is the kernel iogpu.wired_limit_mb the smallest // of the three candidates? When yes we swap "→ ceiling" for // "/ effective ceiling X (kernel limit)" so the user knows // why the value isn't what their tier math suggested. const kernelBinds = (candidates, finalCeiling) => metalCapGB > 0 && Math.abs(metalCapGB - finalCeiling) < 1e-6 && candidates.every((c) => c >= metalCapGB - 1e-6); if (tier === 'custom') { const custom = Number( this.globalSettings.memory?.memory_guard_custom_ceiling_gb || 0 ); const candidates = [custom, staticCeiling]; if (metalCapGB > 0) candidates.push(metalCapGB); const ceiling = Math.max(0, Math.min(...candidates)); const tmpl = kernelBinds([custom, staticCeiling], ceiling) ? 'settings.resource.guard_tier.breakdown_custom_kernel_limit' : 'settings.resource.guard_tier.breakdown_custom'; return window .t(tmpl) .replace('{custom_gb}', bold(custom)) .replace('{ceiling}', bold(ceiling)); } const freeGB = (sys.free_memory_bytes || 0) / GB; const inactiveGB = (sys.inactive_memory_bytes || 0) / GB; const activeGB = (sys.active_memory_bytes || 0) / GB; const ratio = { safe: 0.2, balanced: 0.5, aggressive: 0.8 }[tier] ?? 0.5; const pct = Math.round(ratio * 100); const reclaim = activeGB * ratio; const omlxGB = (sys.omlx_phys_footprint_bytes || 0) / GB; const dynamicCeiling = omlxGB + freeGB + inactiveGB + reclaim; const candidates = [dynamicCeiling, staticCeiling]; if (metalCapGB > 0) candidates.push(metalCapGB); const ceiling = Math.max(0, Math.min(...candidates)); const tmpl = kernelBinds([dynamicCeiling, staticCeiling], ceiling) ? 'settings.resource.guard_tier.breakdown_kernel_limit' : 'settings.resource.guard_tier.breakdown'; return window .t(tmpl) .replace('{free}', bold(freeGB)) .replace('{inactive}', bold(inactiveGB)) .replace('{active}', bold(activeGB)) .replace(/{active_pct}/g, pct) .replace('{reclaim}', bold(reclaim)) .replace('{ceiling}', bold(ceiling)); }, // Computed hot cache size in GB (for manual input) get hotCacheSizeGB() { const val = this.globalSettings.cache?.hot_cache_max_size; if (val && val !== '0') { const parsed = this._parseSettingsGB(val); if (parsed !== null) return parsed; } if (this.hotCachePercent === 0) return 0; const totalBytes = this.globalSettings.system?.total_memory_bytes || 0; const bytes = Math.floor((this.hotCachePercent / 100) * totalBytes); return Math.floor(bytes / (1024 * 1024 * 1024)); }, // Update hot cache from manual GB input updateHotCacheFromInput(gbValue) { const gb = parseInt(gbValue) || 0; if (gb === 0) { this.hotCachePercent = 0; this.globalSettings.cache.hot_cache_max_size = '0'; } else { const totalBytes = this.globalSettings.system?.total_memory_bytes || 0; if (totalBytes > 0) { const bytes = gb * 1024 * 1024 * 1024; this.hotCachePercent = Math.min(50, Math.max(1, Math.round((bytes / totalBytes) * 100))); } this.globalSettings.cache.hot_cache_max_size = `${gb}GB`; } }, // Computed cache size in GB (for manual input) get cacheSizeGB() { const val = this.globalSettings.cache?.ssd_cache_max_size; if (val && val !== 'auto') { const parsed = this._parseSettingsGB(val); if (parsed !== null) return parsed; } const totalBytes = this.globalSettings.system?.ssd_total_bytes || 0; if (!totalBytes) return 0; const bytes = Math.floor((this.cachePercent / 100) * totalBytes); return Math.round(bytes / (1024 * 1024 * 1024)); }, // Update cache from slider updateCacheFromSlider() { const totalBytes = this.globalSettings.system?.ssd_total_bytes || 0; this.globalSettings.cache.ssd_cache_max_size = this.percentToCacheString(this.cachePercent, totalBytes); }, // Update cache from manual GB input updateCacheFromInput(gbValue) { const gb = parseInt(gbValue) || 0; this.globalSettings.cache.ssd_cache_max_size = `${gb}GB`; // Update percent slider const totalBytes = this.globalSettings.system?.ssd_total_bytes || 0; if (totalBytes > 0) { const bytes = gb * 1024 * 1024 * 1024; this.cachePercent = Math.min(100, Math.round((bytes / totalBytes) * 100)); } }, // Parse hot cache size string to percent of total memory normalizeHotCacheMaxSize(value) { const normalized = String(value ?? '').trim(); if (!normalized || normalized.toLowerCase() === 'auto') return '0'; return normalized; }, parseHotCacheToPercent(hotCacheStr, totalBytes) { if (!hotCacheStr || hotCacheStr === '0' || !totalBytes || totalBytes === 0) { return 0; } const match = hotCacheStr.match(/^(\d+(?:\.\d+)?)\s*(GB|MB|TB)?$/i); if (!match) return 0; let bytes = parseFloat(match[1]); const unit = (match[2] || 'GB').toUpperCase(); if (unit === 'TB') bytes *= 1024 * 1024 * 1024 * 1024; else if (unit === 'GB') bytes *= 1024 * 1024 * 1024; else if (unit === 'MB') bytes *= 1024 * 1024; const percent = Math.round((bytes / totalBytes) * 100); return Math.min(50, Math.max(0, percent)); }, // Update hot cache setting from slider updateHotCacheFromSlider() { if (this.hotCachePercent === 0) { this.globalSettings.cache.hot_cache_max_size = '0'; } else { const totalBytes = this.globalSettings.system?.total_memory_bytes || 0; const bytes = Math.floor((this.hotCachePercent / 100) * totalBytes); const gb = Math.floor(bytes / (1024 * 1024 * 1024)); this.globalSettings.cache.hot_cache_max_size = gb > 0 ? `${gb}GB` : '0'; } }, // Get formatted hot cache size for display getHotCacheDisplay() { if (this.hotCachePercent === 0) return '0GB'; const totalBytes = this.globalSettings.system?.total_memory_bytes || 0; const bytes = Math.floor((this.hotCachePercent / 100) * totalBytes); const gb = Math.floor(bytes / (1024 * 1024 * 1024)); return `${gb}GB`; }, // Filter + sort models get sortedModels() { return [...this.filterModelsByName(this.models, this.modelSearch)].sort((a, b) => { // Favorites always sort first, regardless of the active column. const favDiff = (b.is_favorite ? 1 : 0) - (a.is_favorite ? 1 : 0); if (favDiff !== 0) return favDiff; let aVal, bVal; switch (this.sortBy) { case 'id': aVal = (a.display_name || a.id || '').toLowerCase(); bVal = (b.display_name || b.id || '').toLowerCase(); break; case 'type': aVal = (a.model_type || 'llm').toLowerCase(); bVal = (b.model_type || 'llm').toLowerCase(); break; case 'size': aVal = a.estimated_size || 0; bVal = b.estimated_size || 0; break; case 'loaded': aVal = a.loaded ? 1 : 0; bVal = b.loaded ? 1 : 0; break; case 'pinned': aVal = a.pinned ? 1 : 0; bVal = b.pinned ? 1 : 0; break; case 'is_default': aVal = a.is_default ? 1 : 0; bVal = b.is_default ? 1 : 0; break; case 'is_hidden': aVal = a.is_hidden ? 1 : 0; bVal = b.is_hidden ? 1 : 0; break; default: return 0; } if (aVal < bVal) return this.sortOrder === 'asc' ? -1 : 1; if (aVal > bVal) return this.sortOrder === 'asc' ? 1 : -1; return 0; }); }, toggleSort(column) { if (this.sortBy === column) { this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc'; } else { this.sortBy = column; this.sortOrder = 'asc'; } this.persistSort('omlx_models_sort_by', this.sortBy, 'omlx_models_sort_order', this.sortOrder); }, resetSort() { this.sortBy = MODELS_SORT_DEFAULT.by; this.sortOrder = MODELS_SORT_DEFAULT.order; try { localStorage.removeItem('omlx_models_sort_by'); localStorage.removeItem('omlx_models_sort_order'); } catch (e) { /* storage disabled */ } }, get isModelsSortDefault() { return this.sortBy === MODELS_SORT_DEFAULT.by && this.sortOrder === MODELS_SORT_DEFAULT.order; }, // ---- Manager (Browse Models > Local) filter + sort ---- // Cross-reference the richer /api/models entry (has model_type, // settings) for a manager row keyed by its model name. managerModelInfo(name) { return this.models.find(m => m.id === name); }, filterModelsByName(list, query) { const q = (query || '').trim().toLowerCase(); if (!q) return list; return list.filter(m => { const id = (m.id || m.name || '').toLowerCase(); const display = (m.display_name || '').toLowerCase(); const alias = ( (m.settings && m.settings.model_alias) || (this.managerModelInfo(m.name) && this.managerModelInfo(m.name).settings && this.managerModelInfo(m.name).settings.model_alias) || '' ).toLowerCase(); return id.includes(q) || display.includes(q) || alias.includes(q); }); }, get sortedManagerModels() { const list = this.filterModelsByName(this.hfModels, this.managerSearch); return [...list].sort((a, b) => { // Favorites always sort first, regardless of the active column. const aFav = this.managerModelInfo(a.name)?.is_favorite ? 1 : 0; const bFav = this.managerModelInfo(b.name)?.is_favorite ? 1 : 0; if (aFav !== bFav) return bFav - aFav; let aVal, bVal; switch (this.managerSortBy) { case 'name': aVal = (a.display_name || a.name || '').toLowerCase(); bVal = (b.display_name || b.name || '').toLowerCase(); break; case 'type': aVal = (this.managerModelInfo(a.name)?.model_type || 'llm').toLowerCase(); bVal = (this.managerModelInfo(b.name)?.model_type || 'llm').toLowerCase(); break; case 'size': aVal = a.size || 0; bVal = b.size || 0; break; default: return 0; } if (aVal < bVal) return this.managerSortOrder === 'asc' ? -1 : 1; if (aVal > bVal) return this.managerSortOrder === 'asc' ? 1 : -1; return 0; }); }, toggleManagerSort(column) { if (this.managerSortBy === column) { this.managerSortOrder = this.managerSortOrder === 'asc' ? 'desc' : 'asc'; } else { this.managerSortBy = column; this.managerSortOrder = 'asc'; } this.persistSort('omlx_manager_sort_by', this.managerSortBy, 'omlx_manager_sort_order', this.managerSortOrder); }, resetManagerSort() { this.managerSortBy = MANAGER_SORT_DEFAULT.by; this.managerSortOrder = MANAGER_SORT_DEFAULT.order; try { localStorage.removeItem('omlx_manager_sort_by'); localStorage.removeItem('omlx_manager_sort_order'); } catch (e) { /* storage disabled */ } }, get isManagerSortDefault() { return this.managerSortBy === MANAGER_SORT_DEFAULT.by && this.managerSortOrder === MANAGER_SORT_DEFAULT.order; }, persistSort(byKey, byVal, orderKey, orderVal) { try { localStorage.setItem(byKey, byVal); localStorage.setItem(orderKey, orderVal); } catch (e) { /* storage disabled */ } }, // Deeplink from a manager row to that model's settings card (modal). openModelSettingsFromManager(name) { const model = this.managerModelInfo(name); if (model) this.openModelSettings(model); }, // Theme select setTheme(theme) { this.theme = theme; localStorage.setItem('omlx-chat-theme', this.theme); this.applyTheme(); }, applyTheme() { // Clean up existing listener if (this.systemThemeListener) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.systemThemeListener); this.systemThemeListener = null; } if (this.theme === 'auto') { // Detect system theme const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; this.activeTheme = prefersDark ? 'dark' : 'light'; document.documentElement.setAttribute('data-theme', this.activeTheme); // Add listener for system theme changes this.systemThemeListener = (e) => { this.activeTheme = e.matches ? 'dark' : 'light'; document.documentElement.setAttribute('data-theme', this.activeTheme); }; window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', this.systemThemeListener); } else { // Use explicit theme this.activeTheme = this.theme; document.documentElement.setAttribute('data-theme', this.activeTheme); } }, // ================================================================= // HuggingFace Mirror Settings // ================================================================= openHfMirrorModal() { this.hfMirrorEndpoint = this.globalSettings.huggingface.endpoint || ''; this.showHfMirrorModal = true; }, async saveHfMirrorEndpoint() { this.hfMirrorSaving = true; try { const response = await fetch('/admin/api/global-settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ hf_endpoint: this.hfMirrorEndpoint }), }); if (response.ok) { this.globalSettings.huggingface.endpoint = this.hfMirrorEndpoint; this.showHfMirrorModal = false; } else if (response.status === 401) { window.location.href = '/admin'; } else { const data = await response.json(); alert(Array.isArray(data.detail) ? data.detail.map(e => (e && typeof e === 'object') ? (e.msg || JSON.stringify(e)) : String(e)).join(', ') : (data.detail || 'Failed to save')); } } catch (err) { console.error('Failed to save HF mirror endpoint:', err); } finally { this.hfMirrorSaving = false; } }, // ================================================================= // HuggingFace Downloader Functions // ================================================================= async startHFDownload() { const repoId = this.hfRepoId.trim(); if (!repoId) return; this.hfError = ''; this.hfSuccess = ''; this.hfDownloading = true; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 15000); try { const response = await fetch('/admin/api/hf/download', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ repo_id: repoId, hf_token: this.hfToken, }), signal: controller.signal, }); if (response.ok) { this.hfSuccess = window.t('js.success.download_started').replace('{repo_id}', repoId); this.hfRepoId = ''; await this.loadHFTasks(); this.startHFRefresh(); setTimeout(() => { this.hfSuccess = ''; }, 5000); } else if (response.status === 401) { window.location.href = '/admin'; } else { const data = await response.json().catch(() => ({})); this.hfError = data.detail || window.t('js.error.start_download_failed'); } } catch (err) { if (err.name === 'AbortError') { this.hfError = 'HuggingFace request timed out. The service may be unavailable.'; } else { this.hfError = window.t('js.error.start_download_connection'); } console.error('Failed to start download:', err); } finally { clearTimeout(timeoutId); this.hfDownloading = false; } }, async loadHFTasks() { try { const response = await fetch('/admin/api/hf/tasks'); if (response.ok) { const data = await response.json(); this.hfTasks = data.tasks || []; // Stop refresh if no active downloads const hasActive = this.hfTasks.some(t => t.status === 'pending' || t.status === 'downloading'); if (!hasActive) { this.stopHFRefresh(); // Refresh model lists when all downloads finish if (this.hfTasks.some(t => t.status === 'completed')) { await this.loadHFModels(); await this.loadModels(); } } } else if (response.status === 401) { window.location.href = '/admin'; } } catch (err) { console.error('Failed to load HF tasks:', err); } }, async loadHFModels() { try { const response = await fetch('/admin/api/hf/models'); if (response.ok) { const data = await response.json(); this.hfModels = data.models || []; this.hfModelsLoaded = true; } else if (response.status === 401) { window.location.href = '/admin'; } } catch (err) { console.error('Failed to load HF models:', err); } }, async cancelHFDownload(taskId) { try { const response = await fetch(`/admin/api/hf/cancel/${taskId}`, { method: 'POST', }); if (response.ok) { await this.loadHFTasks(); } } catch (err) { console.error('Failed to cancel download:', err); } }, async retryHFDownload(taskId) { try { const response = await fetch(`/admin/api/hf/retry/${taskId}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ hf_token: this.hfToken || '' }), }); if (response.ok) { await this.loadHFTasks(); this.startHFRefresh(); } else { const data = await response.json().catch(() => ({})); this.hfError = data.detail || 'Retry failed'; setTimeout(() => { this.hfError = ''; }, 5000); } } catch (err) { console.error('Failed to retry download:', err); } }, async removeHFTask(taskId) { try { const response = await fetch(`/admin/api/hf/task/${taskId}`, { method: 'DELETE', }); if (response.ok) { await this.loadHFTasks(); } } catch (err) { console.error('Failed to remove task:', err); } }, async deleteHFModel(modelName) { this.hfDeleteConfirm = null; try { const response = await fetch(`/admin/api/hf/models/${encodeURIComponent(modelName)}`, { method: 'DELETE', }); if (response.ok) { await this.loadHFModels(); await this.loadModels(); } else { const data = await response.json(); this.hfError = data.detail || window.t('js.error.delete_model_failed'); setTimeout(() => { this.hfError = ''; }, 5000); } } catch (err) { console.error('Failed to delete model:', err); this.hfError = window.t('js.error.delete_model_connection'); setTimeout(() => { this.hfError = ''; }, 5000); } }, startHFRefresh() { this.stopHFRefresh(); this._hfRefreshTimer = setInterval(() => { this.loadHFTasks(); }, 2000); }, stopHFRefresh() { if (this._hfRefreshTimer) { clearInterval(this._hfRefreshTimer); this._hfRefreshTimer = null; } }, formatProgress(task) { const pct = Math.round(task.progress || 0); const dlGB = (task.downloaded_size / (1024 ** 3)).toFixed(1); const totalGB = (task.total_size / (1024 ** 3)).toFixed(1); return `${pct}% \u00b7 ${dlGB} GB / ${totalGB} GB`; }, // ================================================================= // oQ Quantization Functions // ================================================================= async loadOQModels() { try { const response = await fetch('/admin/api/oq/models'); if (response.ok) { const data = await response.json(); this.oqModels = data.models || []; this.oqAllModels = data.all_models || []; this.oqModelsLoaded = true; } } catch (err) { console.error('Failed to load quantizable models:', err); } }, async startOQQuantization() { if (!this.oqSelectedModelPath || this.oqStarting) return; this.oqError = ''; this.oqSuccess = ''; this.oqStarting = true; try { const payload = { model_path: this.oqSelectedModelPath, oq_level: this.oqLevel, group_size: 64, sensitivity_model_path: this.oqSensitivityModelPath, text_only: this.oqTextOnly, dtype: this.oqDtype, preserve_mtp: this.oqSelectedModelHasMtp() ? this.oqPreserveMtp : false, }; if (this.oqEnhanced) { payload.enhanced = true; payload.imatrix_reuse_cache = this.oqeReuseImatrixCache; payload.imatrix_cache_path = this.oqeImatrixCachePath.trim(); payload.imatrix_strict = this.oqeStrictImatrix; } const response = await fetch('/admin/api/oq/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); const data = await response.json().catch(() => ({})); if (response.ok) { const model = this.oqModels.find(m => m.path === this.oqSelectedModelPath); const name = model ? model.name : this.oqSelectedModelPath; this.oqSuccess = `Quantization started: ${name} → oQ${this.oqLevel}${this.oqEnhanced ? 'e' : ''}`; await this.loadOQTasks(); this.startOQRefresh(); setTimeout(() => { this.oqSuccess = ''; }, 5000); } else { this.oqError = data.detail || 'Failed to start quantization'; } } catch (err) { this.oqError = 'Connection error. Server may be unavailable.'; } finally { this.oqStarting = false; } }, async loadOQTasks() { try { const response = await fetch('/admin/api/oq/tasks'); if (response.ok) { const data = await response.json(); this.oqTasks = data.tasks || []; const hasActive = this.oqTasks.some(t => ['pending', 'loading', 'quantizing', 'saving'].includes(t.status)); if (!hasActive) { this.stopOQRefresh(); if (this.oqTasks.some(t => t.status === 'completed')) { await this.loadHFModels(); await this.loadModels(); await this.loadOQModels(); } } } } catch (err) { console.error('Failed to load oQ tasks:', err); } }, async cancelOQTask(taskId) { try { await fetch(`/admin/api/oq/cancel/${taskId}`, { method: 'POST' }); await this.loadOQTasks(); } catch (err) { console.error('Failed to cancel oQ task:', err); } }, async removeOQTask(taskId) { try { await fetch(`/admin/api/oq/task/${taskId}`, { method: 'DELETE' }); await this.loadOQTasks(); } catch (err) { console.error('Failed to remove oQ task:', err); } }, startOQRefresh() { this.stopOQRefresh(); this._oqRefreshTimer = setInterval(() => { this.loadOQTasks(); }, 2000); }, stopOQRefresh() { if (this._oqRefreshTimer) { clearInterval(this._oqRefreshTimer); this._oqRefreshTimer = null; } }, formatOQProgress(task) { const pct = Math.round(task.progress || 0); const label = task.progress_detail || task.phase || task.status; return `${pct}% · ${label}`; }, formatOQElapsed(task) { if (!task.started_at) return ''; const now = task.completed_at || (Date.now() / 1000); const elapsed = now - task.started_at; const mins = Math.floor(elapsed / 60); const secs = Math.floor(elapsed % 60); return `${mins}:${String(secs).padStart(2, '0')}`; }, oqSensitivityModelCandidates() { if (!this.oqSelectedModelPath) return []; const source = this.oqModels.find(m => m.path === this.oqSelectedModelPath); if (!source) return []; return this.oqAllModels.filter(m => m.path !== this.oqSelectedModelPath && m.is_quantized && m.model_type === source.model_type ); }, oqLevelLabel(level) { return `oQ${level}${this.oqEnhanced ? 'e' : ''}`; }, oqSelectedModelIsVLM() { const model = this.oqModels.find(m => m.path === this.oqSelectedModelPath); return model?.is_vlm || false; }, oqSelectedModelHasMtp() { const model = this.oqModels.find(m => m.path === this.oqSelectedModelPath); return model?.has_mtp_heads || false; }, oqEstimatedMemory() { // Use precise estimate from API if available if (this.oqEstimate) { // If sensitivity model selected, memory ≈ sensitivity model size × 1.5 if (this.oqSensitivityModelPath) { const sensModel = this.oqAllModels.find(m => m.path === this.oqSensitivityModelPath); if (sensModel) { const bytes = Math.round(sensModel.size * 1.5) + 5 * 1024 * 1024 * 1024; if (bytes > 1024 * 1024 * 1024) return (bytes / (1024 * 1024 * 1024)).toFixed(1) + ' GB'; return (bytes / (1024 * 1024)).toFixed(0) + ' MB'; } } return this.oqEstimate.memory_streaming_formatted || ''; } // Fallback to rough model-level estimate const model = this.oqModels.find(m => m.path === this.oqSelectedModelPath); if (!model) return ''; return model.memory_streaming?.peak_formatted || ''; }, oqEstimate: null, _oqEstimateTimer: null, oqEstimatedBpw() { return this.oqEstimate?.effective_bpw?.toFixed(1) || ''; }, oqEstimatedOutputSize() { return this.oqEstimate?.output_size_formatted || ''; }, oqRefreshEstimate() { // Debounce: wait 300ms after last change if (this._oqEstimateTimer) clearTimeout(this._oqEstimateTimer); if (!this.oqSelectedModelPath) { this.oqEstimate = null; return; } this._oqEstimateTimer = setTimeout(async () => { try { const params = new URLSearchParams({ model_path: this.oqSelectedModelPath, oq_level: this.oqLevel, preserve_mtp: this.oqSelectedModelHasMtp() && this.oqPreserveMtp ? 'true' : 'false', }); const resp = await fetch(`/admin/api/oq/estimate?${params}`); if (resp.ok) { this.oqEstimate = await resp.json(); } } catch (e) { console.error('Failed to estimate oQ:', e); } }, 300); }, // ================================================================= // oQ Uploader Functions // ================================================================= async validateUploadToken() { if (!this.uploadHfToken || this.uploadTokenValidating) return; this.uploadTokenValidating = true; this.uploadError = ''; try { const response = await fetch('/admin/api/upload/validate-token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ hf_token: this.uploadHfToken }), }); const data = await response.json().catch(() => ({})); if (response.ok) { this.uploadHfUsername = data.username || ''; this.uploadHfOrgs = data.orgs || []; this.uploadHfNamespace = this.uploadHfUsername; this.uploadTokenValidated = true; localStorage.setItem('omlx-hf-upload-token', this.uploadHfToken); this.loadUploadOqModels(); } else { this.uploadError = data.detail || window.t('models.uploader.invalid_token'); this.uploadTokenValidated = false; } } catch (err) { this.uploadError = 'Connection error. Server may be unavailable.'; } finally { this.uploadTokenValidating = false; } }, async loadUploadOqModels() { try { const response = await fetch('/admin/api/upload/oq-models'); if (response.ok) { const data = await response.json(); this.uploadOqModels = data.oq_models || []; this.uploadAllModels = data.all_models || []; this.uploadOqModelsLoaded = true; } } catch (err) { console.error('Failed to load oQ models for upload:', err); } }, openUploadModal(model) { this.uploadModalModelPath = model.path; this.uploadModalModelName = model.name; this.uploadModalRepoId = (this.uploadHfNamespace || this.uploadHfUsername) + '/' + model.name; this.uploadReadmeSource = ''; this.uploadAutoReadme = true; this.uploadRedownloadNotice = false; this.uploadPrivate = false; this.uploadStarting = false; this.uploadModalOpen = true; }, async startUpload() { if (!this.uploadModalRepoId || this.uploadStarting) return; this.uploadStarting = true; this.uploadError = ''; try { const response = await fetch('/admin/api/upload/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model_path: this.uploadModalModelPath, repo_id: this.uploadModalRepoId, hf_token: this.uploadHfToken, readme_source_path: this.uploadReadmeSource, auto_readme: this.uploadAutoReadme, redownload_notice: this.uploadRedownloadNotice && this.uploadReadmeSource === '', private: this.uploadPrivate, }), }); const data = await response.json().catch(() => ({})); if (response.ok) { this.uploadModalOpen = false; this.uploadSuccess = `Upload queued: ${this.uploadModalModelName}`; await this.loadUploadTasks(); this.startUploadRefresh(); setTimeout(() => { this.uploadSuccess = ''; }, 5000); } else { this.uploadError = data.detail || 'Failed to start upload'; } } catch (err) { this.uploadError = 'Connection error. Server may be unavailable.'; } finally { this.uploadStarting = false; } }, async loadUploadTasks() { try { const response = await fetch('/admin/api/upload/tasks'); if (response.ok) { const data = await response.json(); this.uploadTasks = data.tasks || []; const hasActive = this.uploadTasks.some(t => ['pending', 'uploading'].includes(t.status)); if (!hasActive) { this.stopUploadRefresh(); } } } catch (err) { console.error('Failed to load upload tasks:', err); } }, async cancelUploadTask(taskId) { try { await fetch(`/admin/api/upload/cancel/${taskId}`, { method: 'POST' }); await this.loadUploadTasks(); } catch (err) { console.error('Failed to cancel upload task:', err); } }, async removeUploadTask(taskId) { try { await fetch(`/admin/api/upload/task/${taskId}`, { method: 'DELETE' }); await this.loadUploadTasks(); } catch (err) { console.error('Failed to remove upload task:', err); } }, startUploadRefresh() { this.stopUploadRefresh(); this._uploadRefreshTimer = setInterval(() => { this.loadUploadTasks(); }, 2000); }, stopUploadRefresh() { if (this._uploadRefreshTimer) { clearInterval(this._uploadRefreshTimer); this._uploadRefreshTimer = null; } }, formatUploadElapsed(task) { if (!task.started_at) return ''; const now = task.completed_at || (Date.now() / 1000); const elapsed = now - task.started_at; const mins = Math.floor(elapsed / 60); const secs = Math.floor(elapsed % 60); return `${mins}:${String(secs).padStart(2, '0')}`; }, // ================================================================= // Recommended Models Functions // ================================================================= async loadRecommendedModels() { this.hfRecommendedLoading = true; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 15000); try { const response = await fetch(`/admin/api/hf/recommended?mlx_only=${this.hfMlxOnly}`, { signal: controller.signal }); if (response.ok) { const data = await response.json(); // Attach original rank so the # column survives column-header re-sorts this.hfRecommended = { trending: (data.trending || []).map((m, i) => ({ ...m, rank: i + 1 })), popular: (data.popular || []).map((m, i) => ({ ...m, rank: i + 1 })), }; this.hfRecommendedLoaded = true; this.hfPage.trending = 1; this.hfPage.popular = 1; // Default sort for trending/popular is original rank this.hfTableSort = 'rank'; this.hfTableSortDir = 'asc'; } else if (response.status === 401) { window.location.href = '/admin'; } else { const data = await response.json().catch(() => ({})); this.hfError = data.detail || 'Failed to load recommended models'; setTimeout(() => { this.hfError = ''; }, 5000); } } catch (err) { if (err.name === 'AbortError') { this.hfError = 'HuggingFace request timed out. The service may be unavailable.'; } else { this.hfError = 'Failed to connect to HuggingFace.'; } setTimeout(() => { this.hfError = ''; }, 5000); console.error('Failed to load recommended models:', err); } finally { clearTimeout(timeoutId); this.hfRecommendedLoading = false; } }, downloadRecommended(repoId) { this.hfRepoId = repoId; this.startHFDownload(); }, getMemoryFitStatus(sizeBytes) { const totalBytes = this.globalSettings.system?.total_memory_bytes || 0; if (!totalBytes || !sizeBytes) return 'safe'; const ratio = sizeBytes / totalBytes; if (ratio > 0.95) return 'danger'; if (ratio > 0.80) return 'warning'; return 'safe'; }, formatDownloads(count) { if (count >= 1000000) return (count / 1000000).toFixed(1) + 'M'; if (count >= 1000) return (count / 1000).toFixed(1) + 'K'; return count.toString(); }, // Table sort helpers for Browse Models sortModels(list) { const sortBy = this.hfTableSort; const dir = this.hfTableSortDir === 'asc' ? 1 : -1; return [...list].sort((a, b) => { if (sortBy === 'rank') { return dir * ((a.rank || 0) - (b.rank || 0)); } else if (sortBy === 'name') { return dir * (a.name || '').localeCompare(b.name || ''); } else if (sortBy === 'downloads') { return dir * ((a.downloads || 0) - (b.downloads || 0)); } else if (sortBy === 'likes') { return dir * ((a.likes || 0) - (b.likes || 0)); } else if (sortBy === 'size') { return dir * ((a.size || 0) - (b.size || 0)); } else if (sortBy === 'params') { return dir * ((a.params || 0) - (b.params || 0)); } return 0; }); }, toggleTableSort(column) { if (this.hfTableSort === column) { this.hfTableSortDir = this.hfTableSortDir === 'asc' ? 'desc' : 'asc'; } else { this.hfTableSort = column; // Name and rank read more naturally as ascending by default this.hfTableSortDir = (column === 'name' || column === 'rank') ? 'asc' : 'desc'; } }, syncTableSortToDropdown() { const map = { largest: { col: 'size', dir: 'desc' }, smallest: { col: 'size', dir: 'asc' }, most_params: { col: 'params', dir: 'desc' }, least_params: { col: 'params', dir: 'asc' }, downloads: { col: 'downloads', dir: 'desc' }, trending: { col: 'downloads', dir: 'desc' }, created: { col: 'downloads', dir: 'desc' }, updated: { col: 'downloads', dir: 'desc' }, }; const m = map[this.hfSearchSort]; if (m) { this.hfTableSort = m.col; this.hfTableSortDir = m.dir; } }, // Pagination helpers getPagedModels(tab) { const page = this.hfPage[tab] || 1; const size = this.hfPageSize; let list; if (tab === 'trending') list = this.hfRecommended.trending || []; else if (tab === 'popular') list = this.hfRecommended.popular || []; else list = this.hfSearchResults || []; // Apply table sorting const sorted = this.sortModels(list); return sorted.slice((page - 1) * size, page * size); }, getTotalPages(tab) { let total; if (tab === 'trending') total = (this.hfRecommended.trending || []).length; else if (tab === 'popular') total = (this.hfRecommended.popular || []).length; else total = (this.hfSearchResults || []).length; const maxPages = tab === 'search' ? 10 : 5; return Math.min(Math.ceil(total / this.hfPageSize), maxPages); }, setPage(tab, page) { this.hfPage[tab] = page; }, // Search async searchHFModels() { if (!this.hfSearchQuery.trim()) return; this.hfSearchLoading = true; this.hfRecommendedTab = 'search'; this.hfPage.search = 1; // Sync table sort with dropdown choice so the frontend re-sort // does not override what the backend returned this.syncTableSortToDropdown(); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 15000); try { const params = new URLSearchParams({ q: this.hfSearchQuery, sort: this.hfSearchSort, limit: '100', mlx_only: this.hfMlxOnly, }); // Add filter parameters if set. Sizes use binary GiB to match // _format_model_size on the backend. const GIB = 1024 * 1024 * 1024; if (this.hfSearchMinParams) params.set('min_params', (parseFloat(this.hfSearchMinParams) * 1e9).toString()); if (this.hfSearchMaxParams) params.set('max_params', (parseFloat(this.hfSearchMaxParams) * 1e9).toString()); if (this.hfSearchMaxSize) params.set('max_size', (parseFloat(this.hfSearchMaxSize) * GIB).toString()); if (this.hfSearchMinSize) params.set('min_size', (parseFloat(this.hfSearchMinSize) * GIB).toString()); // Wire largest/smallest sort params to backend if (this.hfSearchSort === 'largest') { params.set('sort_by_size', 'true'); params.set('sort_ascending', 'false'); } else if (this.hfSearchSort === 'smallest') { params.set('sort_by_size', 'true'); params.set('sort_ascending', 'true'); } const response = await fetch(`/admin/api/hf/search?${params}`, { signal: controller.signal }); if (response.ok) { const data = await response.json(); this.hfSearchResults = data.models || []; this.hfSearchLoaded = true; // Save to search history this.addSearchHistory(this.hfSearchQuery.trim()); } else if (response.status === 401) { window.location.href = '/admin'; } else { const data = await response.json().catch(() => ({})); this.hfError = data.detail || 'Search failed'; setTimeout(() => { this.hfError = ''; }, 5000); } } catch (err) { if (err.name === 'AbortError') { this.hfError = 'HuggingFace request timed out. The service may be unavailable.'; } else { this.hfError = 'Failed to connect to HuggingFace.'; } setTimeout(() => { this.hfError = ''; }, 5000); console.error('Search failed:', err); } finally { clearTimeout(timeoutId); this.hfSearchLoading = false; } }, clearHFSearchFilters() { this.hfSearchMinParams = ''; this.hfSearchMaxParams = ''; this.hfSearchMaxSize = ''; this.hfSearchMinSize = ''; if (this.hfSearchQuery.trim()) this.immediateSearch(); }, debounceSearch() { clearTimeout(this.hfSearchDebounceTimer); if (!this.hfSearchQuery.trim()) return; this.hfSearchDebounceTimer = setTimeout(() => this.searchHFModels(), 500); }, immediateSearch() { clearTimeout(this.hfSearchDebounceTimer); this.searchHFModels(); }, formatParamCount(params) { if (!params) return null; if (params >= 1e12) return (params / 1e12).toFixed(1) + 'T'; if (params >= 1e9) return (params / 1e9).toFixed(1) + 'B'; if (params >= 1e6) return (params / 1e6).toFixed(1) + 'M'; return params.toString(); }, // Search history addSearchHistory(query) { let history = this.hfSearchHistory.filter(h => h !== query); history.unshift(query); history = history.slice(0, 5); this.hfSearchHistory = history; localStorage.setItem('hfSearchHistory', JSON.stringify(history)); }, selectSearchHistory(query) { this.hfSearchQuery = query; this.hfSearchHistoryOpen = false; this.searchHFModels(); }, closeSearchHistory() { setTimeout(() => { this.hfSearchHistoryOpen = false; }, 150); }, // Model detail modal async openModelDetail(repoId) { this.hfModelDetailLoading = true; this.hfModelDetail = null; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 15000); try { const params = new URLSearchParams({ repo_id: repoId }); const response = await fetch(`/admin/api/hf/model-info?${params}`, { signal: controller.signal }); if (response.ok) { this.hfModelDetail = await response.json(); } else if (response.status === 401) { window.location.href = '/admin'; } else { const data = await response.json().catch(() => ({})); this.hfError = data.detail || 'Failed to fetch model info'; setTimeout(() => { this.hfError = ''; }, 5000); } } catch (err) { if (err.name === 'AbortError') { this.hfError = 'HuggingFace request timed out. The service may be unavailable.'; } else { this.hfError = 'Failed to connect to HuggingFace.'; } setTimeout(() => { this.hfError = ''; }, 5000); console.error('Failed to fetch model info:', err); } finally { clearTimeout(timeoutId); this.hfModelDetailLoading = false; } }, closeModelDetail() { this.hfModelDetail = null; this.hfModelDetailLoading = false; this.msModelDetail = null; this.msModelDetailLoading = false; }, formatFileSize(bytes) { if (!bytes) return ''; if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; return (bytes / (1024 * 1024 * 1024)).toFixed(1) + ' GB'; }, // ================================================================= // ModelScope Downloader Functions // ================================================================= async initMsDownloader() { if (this.msInitialized) return; this.msInitialized = true; try { const response = await fetch('/admin/api/ms/status'); if (response.ok) { const data = await response.json(); this.msAvailable = data.available === true; } else { this.msAvailable = false; } } catch (err) { this.msAvailable = false; console.error('Failed to check MS status:', err); } if (this.msAvailable) { await this.loadMSTasks(); } }, async startMSDownload() { let repoId = this.msRepoId.trim(); if (!repoId) return; // Default owner to mlx-community if not specified if (!repoId.includes('/')) { repoId = 'mlx-community/' + repoId; } this.msError = ''; this.msSuccess = ''; this.msDownloading = true; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 20000); try { const response = await fetch('/admin/api/ms/download', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model_id: repoId, ms_token: this.msToken || '', }), signal: controller.signal, }); if (response.ok) { this.msSuccess = window.t('js.success.download_started').replace('{repo_id}', repoId); this.msRepoId = ''; await this.loadMSTasks(); this.startMSRefresh(); setTimeout(() => { this.msSuccess = ''; }, 5000); } else if (response.status === 401) { window.location.href = '/admin'; } else { const data = await response.json().catch(() => ({})); this.msError = data.detail || window.t('js.error.start_download_failed'); } } catch (err) { if (err.name === 'AbortError') { this.msError = 'ModelScope request timed out. The service may be unavailable.'; } else { this.msError = window.t('js.error.start_download_connection'); } console.error('Failed to start MS download:', err); } finally { clearTimeout(timeoutId); this.msDownloading = false; } }, async loadMSTasks() { try { const response = await fetch('/admin/api/ms/tasks'); if (response.ok) { const data = await response.json(); this.msTasks = data.tasks || []; const hasActive = this.msTasks.some(t => t.status === 'pending' || t.status === 'downloading'); if (!hasActive) { this.stopMSRefresh(); if (this.msTasks.some(t => t.status === 'completed')) { await this.loadHFModels(); await this.loadModels(); } } } else if (response.status === 401) { window.location.href = '/admin'; } } catch (err) { console.error('Failed to load MS tasks:', err); } }, async cancelMSDownload(taskId) { try { const response = await fetch(`/admin/api/ms/cancel/${taskId}`, { method: 'POST', }); if (response.ok) { await this.loadMSTasks(); } } catch (err) { console.error('Failed to cancel MS download:', err); } }, async retryMSDownload(taskId) { try { const response = await fetch(`/admin/api/ms/retry/${taskId}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ms_token: this.msToken || null }), }); if (response.ok) { await this.loadMSTasks(); this.startMSRefresh(); } else { const data = await response.json().catch(() => ({})); this.msError = data.detail || 'Retry failed'; setTimeout(() => { this.msError = ''; }, 5000); } } catch (err) { console.error('Failed to retry MS download:', err); } }, async removeMSTask(taskId) { try { const response = await fetch(`/admin/api/ms/task/${taskId}`, { method: 'DELETE', }); if (response.ok) { await this.loadMSTasks(); } } catch (err) { console.error('Failed to remove MS task:', err); } }, startMSRefresh() { this.stopMSRefresh(); this._msRefreshTimer = setInterval(() => { this.loadMSTasks(); }, 2000); }, stopMSRefresh() { if (this._msRefreshTimer) { clearInterval(this._msRefreshTimer); this._msRefreshTimer = null; } }, downloadMsModel(repoId) { this.msRepoId = repoId; this.startMSDownload(); }, // MS Recommended models async loadMsRecommendedModels() { this.msRecommendedLoading = true; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 20000); try { const response = await fetch(`/admin/api/ms/recommended?mlx_only=${this.msMlxOnly}`, { signal: controller.signal }); if (response.ok) { const data = await response.json(); this.msRecommended = data; this.msRecommendedLoaded = true; this.msPage.trending = 1; this.msPage.popular = 1; } else if (response.status === 401) { window.location.href = '/admin'; } else { const data = await response.json().catch(() => ({})); this.msError = data.detail || 'Failed to load recommended models'; setTimeout(() => { this.msError = ''; }, 5000); } } catch (err) { if (err.name === 'AbortError') { this.msError = 'ModelScope request timed out. The service may be unavailable.'; } else { this.msError = 'Failed to connect to ModelScope.'; } setTimeout(() => { this.msError = ''; }, 5000); console.error('Failed to load MS recommended models:', err); } finally { clearTimeout(timeoutId); this.msRecommendedLoading = false; } }, // MS Pagination helpers getMsPagedModels(tab) { const page = this.msPage[tab] || 1; const size = this.msPageSize; let list; if (tab === 'trending') list = (this.msRecommended.trending || []); else if (tab === 'popular') list = (this.msRecommended.popular || []); else list = this.msSearchResults || []; return list.slice((page - 1) * size, page * size); }, getMsTotalPages(tab) { let total; if (tab === 'trending') total = (this.msRecommended.trending || []).length; else if (tab === 'popular') total = (this.msRecommended.popular || []).length; else total = (this.msSearchResults || []).length; const maxPages = tab === 'search' ? 10 : 5; return Math.min(Math.ceil(total / this.msPageSize), maxPages); }, setMsPage(tab, page) { this.msPage[tab] = page; }, // MS Search async searchMSModels() { if (!this.msSearchQuery.trim()) return; this.msSearchLoading = true; this.msRecommendedTab = 'search'; this.msPage.search = 1; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 20000); try { const params = new URLSearchParams({ q: this.msSearchQuery, sort: this.msSearchSort, limit: '50', mlx_only: this.msMlxOnly, }); const response = await fetch(`/admin/api/ms/search?${params}`, { signal: controller.signal }); if (response.ok) { const data = await response.json(); this.msSearchResults = data.models || []; this.msSearchLoaded = true; } else if (response.status === 401) { window.location.href = '/admin'; } else { const data = await response.json().catch(() => ({})); this.msError = data.detail || 'Search failed'; setTimeout(() => { this.msError = ''; }, 5000); } } catch (err) { if (err.name === 'AbortError') { this.msError = 'ModelScope request timed out. The service may be unavailable.'; } else { this.msError = 'Failed to connect to ModelScope.'; } setTimeout(() => { this.msError = ''; }, 5000); console.error('MS search failed:', err); } finally { clearTimeout(timeoutId); this.msSearchLoading = false; } }, mImmediateSearch() { const query = this.msSearchQuery.trim(); if (query) { // Save to search history this.msSearchHistory = [query, ...this.msSearchHistory.filter(h => h !== query)].slice(0, 10); localStorage.setItem('msSearchHistory', JSON.stringify(this.msSearchHistory)); } this.msSearchHistoryOpen = false; this.searchMSModels(); }, msDebounceSearch() { clearTimeout(this.msSearchDebounceTimer); this.msSearchDebounceTimer = setTimeout(() => { if (this.msSearchQuery.trim()) { this.searchMSModels(); } }, 500); }, closeMsSearchHistory() { setTimeout(() => { this.msSearchHistoryOpen = false; }, 200); }, selectMsSearchHistory(item) { this.msSearchQuery = item; this.msSearchHistoryOpen = false; this.mImmediateSearch(); }, // MS Model detail modal async openMsModelDetail(repoId) { this.msModelDetailLoading = true; this.msModelDetail = null; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 20000); try { const params = new URLSearchParams({ model_id: repoId }); const response = await fetch(`/admin/api/ms/model-info?${params}`, { signal: controller.signal }); if (response.ok) { this.msModelDetail = await response.json(); } else if (response.status === 401) { window.location.href = '/admin'; } else { const data = await response.json().catch(() => ({})); this.msError = data.detail || 'Failed to fetch model info'; setTimeout(() => { this.msError = ''; }, 5000); } } catch (err) { if (err.name === 'AbortError') { this.msError = 'ModelScope request timed out. The service may be unavailable.'; } else { this.msError = 'Failed to connect to ModelScope.'; } setTimeout(() => { this.msError = ''; }, 5000); console.error('Failed to fetch MS model info:', err); } finally { clearTimeout(timeoutId); this.msModelDetailLoading = false; } }, } }