feat: Enhance configuration management and embedding capabilities

- Added JSON-based settings management in Config class for embedding and LLM configurations.
- Introduced methods to save and load settings from a JSON file.
- Updated BaseEmbedder and its subclasses to include max_tokens property for better token management.
- Enhanced chunking strategy to support recursive splitting of large symbols with improved overlap handling.
- Implemented comprehensive tests for recursive splitting and chunking behavior.
- Added CLI tools configuration management for better integration with external tools.
- Introduced a new command for compacting session memory into structured text for recovery.
This commit is contained in:
catlog22
2025-12-24 16:32:27 +08:00
parent b00113d212
commit e671b45948
25 changed files with 2889 additions and 153 deletions

View File

@@ -170,6 +170,27 @@
letter-spacing: 0.03em;
}
.cli-tool-badge-disabled {
font-size: 0.5625rem;
font-weight: 600;
padding: 0.125rem 0.375rem;
background: hsl(38 92% 50% / 0.2);
color: hsl(38 92% 50%);
border-radius: 9999px;
text-transform: uppercase;
letter-spacing: 0.03em;
}
/* Disabled tool card state */
.cli-tool-card.disabled {
opacity: 0.7;
border-style: dashed;
}
.cli-tool-card.disabled .cli-tool-name {
color: hsl(var(--muted-foreground));
}
.cli-tool-info {
font-size: 0.6875rem;
margin-bottom: 0.3125rem;
@@ -773,6 +794,29 @@
border-color: hsl(var(--destructive) / 0.5);
}
/* Enable/Disable button variants */
.btn-sm.btn-outline-success {
background: transparent;
border: 1px solid hsl(142 76% 36% / 0.4);
color: hsl(142 76% 36%);
}
.btn-sm.btn-outline-success:hover {
background: hsl(142 76% 36% / 0.1);
border-color: hsl(142 76% 36% / 0.6);
}
.btn-sm.btn-outline-warning {
background: transparent;
border: 1px solid hsl(38 92% 50% / 0.4);
color: hsl(38 92% 50%);
}
.btn-sm.btn-outline-warning:hover {
background: hsl(38 92% 50% / 0.1);
border-color: hsl(38 92% 50% / 0.6);
}
/* Empty State */
.empty-state {
display: flex;

View File

@@ -622,11 +622,110 @@ select.cli-input {
align-items: center;
justify-content: flex-end;
gap: 0.75rem;
margin-top: 1rem;
padding-top: 1rem;
margin-top: 1.25rem;
padding-top: 1.25rem;
border-top: 1px solid hsl(var(--border));
}
.modal-actions button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.625rem 1.25rem;
font-size: 0.875rem;
font-weight: 500;
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.2s ease;
min-width: 5rem;
}
.modal-actions .btn-secondary {
background: transparent;
border: 1px solid hsl(var(--border));
color: hsl(var(--muted-foreground));
}
.modal-actions .btn-secondary:hover {
background: hsl(var(--muted));
color: hsl(var(--foreground));
border-color: hsl(var(--muted-foreground) / 0.3);
}
.modal-actions .btn-primary {
background: hsl(var(--primary));
border: 1px solid hsl(var(--primary));
color: hsl(var(--primary-foreground));
}
.modal-actions .btn-primary:hover {
background: hsl(var(--primary) / 0.9);
box-shadow: 0 2px 8px hsl(var(--primary) / 0.3);
}
.modal-actions .btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
box-shadow: none;
}
.modal-actions .btn-danger {
background: hsl(var(--destructive));
border: 1px solid hsl(var(--destructive));
color: hsl(var(--destructive-foreground));
}
.modal-actions .btn-danger:hover {
background: hsl(var(--destructive) / 0.9);
box-shadow: 0 2px 8px hsl(var(--destructive) / 0.3);
}
.modal-actions button i,
.modal-actions button svg {
width: 1rem;
height: 1rem;
flex-shrink: 0;
}
/* Handle .btn class prefix */
.modal-actions .btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.625rem 1.25rem;
font-size: 0.875rem;
font-weight: 500;
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.2s ease;
min-width: 5rem;
}
.modal-actions .btn.btn-secondary {
background: transparent;
border: 1px solid hsl(var(--border));
color: hsl(var(--muted-foreground));
}
.modal-actions .btn.btn-secondary:hover {
background: hsl(var(--muted));
color: hsl(var(--foreground));
border-color: hsl(var(--muted-foreground) / 0.3);
}
.modal-actions .btn.btn-primary {
background: hsl(var(--primary));
border: 1px solid hsl(var(--primary));
color: hsl(var(--primary-foreground));
}
.modal-actions .btn.btn-primary:hover {
background: hsl(var(--primary) / 0.9);
box-shadow: 0 2px 8px hsl(var(--primary) / 0.3);
}
/* Button Icon */
.btn-icon {
display: inline-flex;
@@ -1916,4 +2015,84 @@ select.cli-input {
.health-check-grid {
grid-template-columns: 1fr;
}
}
/* ===========================
Model Settings Modal - Endpoint Preview
=========================== */
.endpoint-preview-section {
background: hsl(var(--muted) / 0.3);
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 0.5rem;
}
.endpoint-preview-section h4 {
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0 0 0.75rem 0;
font-size: 0.875rem;
font-weight: 600;
color: hsl(var(--foreground));
}
.endpoint-preview-section h4 i {
width: 16px;
height: 16px;
color: hsl(var(--primary));
}
.endpoint-preview-box {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.625rem 0.75rem;
background: hsl(var(--background));
border: 1px solid hsl(var(--border));
border-radius: 0.375rem;
margin-bottom: 1rem;
}
.endpoint-preview-box code {
flex: 1;
font-family: 'SF Mono', 'Consolas', 'Liberation Mono', monospace;
font-size: 0.8125rem;
color: hsl(var(--primary));
word-break: break-all;
}
.endpoint-preview-box .btn-icon-sm {
flex-shrink: 0;
}
/* Form Section within Modal */
.form-section {
margin-bottom: 1.25rem;
}
.form-section h4 {
margin: 0 0 0.75rem 0;
font-size: 0.8125rem;
font-weight: 600;
color: hsl(var(--muted-foreground));
text-transform: uppercase;
letter-spacing: 0.05em;
}
.form-section:last-of-type {
margin-bottom: 0;
}
/* Capabilities Checkboxes */
.capabilities-checkboxes {
display: flex;
flex-wrap: wrap;
gap: 0.75rem 1.5rem;
}
.capabilities-checkboxes .checkbox-label {
font-size: 0.875rem;
}

View File

@@ -8,6 +8,8 @@ let semanticStatus = { available: false };
let ccwInstallStatus = { installed: true, workflowsInstalled: true, missingFiles: [], installPath: '' };
let defaultCliTool = 'gemini';
let promptConcatFormat = localStorage.getItem('ccw-prompt-format') || 'plain'; // plain, yaml, json
let cliToolsConfig = {}; // CLI tools enable/disable config
let apiEndpoints = []; // API endpoints from LiteLLM config
// Smart Context settings
let smartContextEnabled = localStorage.getItem('ccw-smart-context') === 'true';
@@ -41,6 +43,12 @@ async function loadAllStatuses() {
semanticStatus = data.semantic || { available: false };
ccwInstallStatus = data.ccwInstall || { installed: true, workflowsInstalled: true, missingFiles: [], installPath: '' };
// Load CLI tools config and API endpoints
await Promise.all([
loadCliToolsConfig(),
loadApiEndpoints()
]);
// Update badges
updateCliBadge();
updateCodexLensBadge();
@@ -168,6 +176,67 @@ async function loadInstalledModels() {
}
}
/**
* Load CLI tools config from .claude/cli-tools.json (project or global fallback)
*/
async function loadCliToolsConfig() {
try {
const response = await fetch('/api/cli/tools-config');
if (!response.ok) return null;
const data = await response.json();
// Store full config and extract tools for backward compatibility
cliToolsConfig = data.tools || {};
window.claudeCliToolsConfig = data; // Full config available globally
// Load default tool from config
if (data.defaultTool) {
defaultCliTool = data.defaultTool;
}
console.log('[CLI Config] Loaded from:', data._configInfo?.source || 'unknown', '| Default:', data.defaultTool);
return data;
} catch (err) {
console.error('Failed to load CLI tools config:', err);
return null;
}
}
/**
* Update CLI tool enabled status
*/
async function updateCliToolEnabled(tool, enabled) {
try {
const response = await fetch('/api/cli/tools-config/' + tool, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: enabled })
});
if (!response.ok) throw new Error('Failed to update');
showRefreshToast(tool + (enabled ? ' enabled' : ' disabled'), 'success');
return await response.json();
} catch (err) {
console.error('Failed to update CLI tool:', err);
showRefreshToast('Failed to update ' + tool, 'error');
return null;
}
}
/**
* Load API endpoints from LiteLLM config
*/
async function loadApiEndpoints() {
try {
const response = await fetch('/api/litellm-api/endpoints');
if (!response.ok) return [];
const data = await response.json();
apiEndpoints = data.endpoints || [];
return apiEndpoints;
} catch (err) {
console.error('Failed to load API endpoints:', err);
return [];
}
}
// ========== Badge Update ==========
function updateCliBadge() {
const badge = document.getElementById('badgeCliTools');
@@ -234,25 +303,41 @@ function renderCliStatus() {
const status = cliToolStatus[tool] || {};
const isAvailable = status.available;
const isDefault = defaultCliTool === tool;
const config = cliToolsConfig[tool] || { enabled: true };
const isEnabled = config.enabled !== false;
const canSetDefault = isAvailable && isEnabled && !isDefault;
return `
<div class="cli-tool-card tool-${tool} ${isAvailable ? 'available' : 'unavailable'}">
<div class="cli-tool-card tool-${tool} ${isAvailable ? 'available' : 'unavailable'} ${!isEnabled ? 'disabled' : ''}">
<div class="cli-tool-header">
<span class="cli-tool-status ${isAvailable ? 'status-available' : 'status-unavailable'}"></span>
<span class="cli-tool-status ${isAvailable && isEnabled ? 'status-available' : 'status-unavailable'}"></span>
<span class="cli-tool-name">${tool.charAt(0).toUpperCase() + tool.slice(1)}</span>
${isDefault ? '<span class="cli-tool-badge">Default</span>' : ''}
${!isEnabled && isAvailable ? '<span class="cli-tool-badge-disabled">Disabled</span>' : ''}
</div>
<div class="cli-tool-desc text-xs text-muted-foreground mt-1">
${toolDescriptions[tool]}
</div>
<div class="cli-tool-info mt-2">
${isAvailable
? `<span class="text-success flex items-center gap-1"><i data-lucide="check-circle" class="w-3 h-3"></i> Ready</span>`
: `<span class="text-muted-foreground flex items-center gap-1"><i data-lucide="circle-dashed" class="w-3 h-3"></i> Not Installed</span>`
}
<div class="cli-tool-info mt-2 flex items-center justify-between">
<div>
${isAvailable
? (isEnabled
? `<span class="text-success flex items-center gap-1"><i data-lucide="check-circle" class="w-3 h-3"></i> Ready</span>`
: `<span class="text-warning flex items-center gap-1"><i data-lucide="pause-circle" class="w-3 h-3"></i> Disabled</span>`)
: `<span class="text-muted-foreground flex items-center gap-1"><i data-lucide="circle-dashed" class="w-3 h-3"></i> Not Installed</span>`
}
</div>
</div>
<div class="cli-tool-actions mt-3">
${isAvailable && !isDefault
<div class="cli-tool-actions mt-3 flex gap-2">
${isAvailable ? (isEnabled
? `<button class="btn-sm btn-outline-warning flex items-center gap-1" onclick="toggleCliTool('${tool}', false)">
<i data-lucide="pause" class="w-3 h-3"></i> Disable
</button>`
: `<button class="btn-sm btn-outline-success flex items-center gap-1" onclick="toggleCliTool('${tool}', true)">
<i data-lucide="play" class="w-3 h-3"></i> Enable
</button>`
) : ''}
${canSetDefault
? `<button class="btn-sm btn-outline flex items-center gap-1" onclick="setDefaultCliTool('${tool}')">
<i data-lucide="star" class="w-3 h-3"></i> Set Default
</button>`
@@ -365,11 +450,42 @@ function renderCliStatus() {
</div>
` : '';
// API Endpoints section
const apiEndpointsHtml = apiEndpoints.length > 0 ? `
<div class="cli-api-endpoints-section" style="margin-top: 1.5rem;">
<div class="cli-section-header" style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 1rem;">
<h4 style="display: flex; align-items: center; gap: 0.5rem; font-weight: 600; margin: 0;">
<i data-lucide="link" class="w-4 h-4"></i> API Endpoints
</h4>
<span class="badge" style="padding: 0.125rem 0.5rem; font-size: 0.75rem; border-radius: 0.25rem; background: var(--muted); color: var(--muted-foreground);">${apiEndpoints.length}</span>
</div>
<div class="cli-endpoints-list" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 0.75rem;">
${apiEndpoints.map(ep => `
<div class="cli-endpoint-card ${ep.enabled ? 'available' : 'unavailable'}" style="padding: 0.75rem; border: 1px solid var(--border); border-radius: 0.5rem; background: var(--card);">
<div class="cli-endpoint-header" style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
<span class="cli-tool-status ${ep.enabled ? 'status-available' : 'status-unavailable'}" style="width: 8px; height: 8px; border-radius: 50%; background: ${ep.enabled ? 'var(--success)' : 'var(--muted-foreground)'}; flex-shrink: 0;"></span>
<span class="cli-endpoint-id" style="font-weight: 500; font-size: 0.875rem;">${ep.id}</span>
</div>
<div class="cli-endpoint-info" style="margin-top: 0.25rem;">
<span class="text-xs text-muted-foreground" style="font-size: 0.75rem; color: var(--muted-foreground);">${ep.model}</span>
</div>
</div>
`).join('')}
</div>
</div>
` : '';
// Config source info
const configInfo = window.claudeCliToolsConfig?._configInfo || {};
const configSourceLabel = configInfo.source === 'project' ? 'Project' : configInfo.source === 'global' ? 'Global' : 'Default';
const configSourceClass = configInfo.source === 'project' ? 'text-success' : configInfo.source === 'global' ? 'text-primary' : 'text-muted-foreground';
// CLI Settings section
const settingsHtml = `
<div class="cli-settings-section">
<div class="cli-settings-header">
<h4><i data-lucide="settings" class="w-3.5 h-3.5"></i> Settings</h4>
<span class="badge text-xs ${configSourceClass}" title="${configInfo.activePath || ''}">${configSourceLabel}</span>
</div>
<div class="cli-settings-grid">
<div class="cli-setting-item">
@@ -436,6 +552,20 @@ function renderCliStatus() {
</div>
<p class="cli-setting-desc">Maximum files to include in smart context</p>
</div>
<div class="cli-setting-item">
<label class="cli-setting-label">
<i data-lucide="hard-drive" class="w-3 h-3"></i>
Cache Injection
</label>
<div class="cli-setting-control">
<select class="cli-setting-select" onchange="setCacheInjectionMode(this.value)">
<option value="auto" ${getCacheInjectionMode() === 'auto' ? 'selected' : ''}>Auto</option>
<option value="manual" ${getCacheInjectionMode() === 'manual' ? 'selected' : ''}>Manual</option>
<option value="disabled" ${getCacheInjectionMode() === 'disabled' ? 'selected' : ''}>Disabled</option>
</select>
</div>
<p class="cli-setting-desc">Cache prefix/suffix injection mode for prompts</p>
</div>
</div>
</div>
`;
@@ -453,6 +583,7 @@ function renderCliStatus() {
${codexLensHtml}
${semanticHtml}
</div>
${apiEndpointsHtml}
${settingsHtml}
`;
@@ -464,7 +595,30 @@ function renderCliStatus() {
// ========== Actions ==========
function setDefaultCliTool(tool) {
// Validate: tool must be available and enabled
const status = cliToolStatus[tool] || {};
const config = cliToolsConfig[tool] || { enabled: true };
if (!status.available) {
showRefreshToast(`Cannot set ${tool} as default: not installed`, 'error');
return;
}
if (config.enabled === false) {
showRefreshToast(`Cannot set ${tool} as default: tool is disabled`, 'error');
return;
}
defaultCliTool = tool;
// Save to config
if (window.claudeCliToolsConfig) {
window.claudeCliToolsConfig.defaultTool = tool;
fetch('/api/cli/tools-config', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ defaultTool: tool })
}).catch(err => console.error('Failed to save default tool:', err));
}
renderCliStatus();
showRefreshToast(`Default CLI tool set to ${tool}`, 'success');
}
@@ -505,11 +659,67 @@ function setRecursiveQueryEnabled(enabled) {
showRefreshToast(`Recursive Query ${enabled ? 'enabled' : 'disabled'}`, 'success');
}
function getCacheInjectionMode() {
if (window.claudeCliToolsConfig && window.claudeCliToolsConfig.settings) {
return window.claudeCliToolsConfig.settings.cache?.injectionMode || 'auto';
}
return localStorage.getItem('ccw-cache-injection-mode') || 'auto';
}
async function setCacheInjectionMode(mode) {
try {
const response = await fetch('/api/cli/tools-config/cache', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ injectionMode: mode })
});
if (response.ok) {
localStorage.setItem('ccw-cache-injection-mode', mode);
if (window.claudeCliToolsConfig) {
window.claudeCliToolsConfig.settings.cache.injectionMode = mode;
}
showRefreshToast(`Cache injection mode set to ${mode}`, 'success');
} else {
showRefreshToast('Failed to update cache settings', 'error');
}
} catch (err) {
console.error('Failed to update cache settings:', err);
showRefreshToast('Failed to update cache settings', 'error');
}
}
async function refreshAllCliStatus() {
await loadAllStatuses();
renderCliStatus();
}
async function toggleCliTool(tool, enabled) {
// If disabling the current default tool, switch to another available+enabled tool
if (!enabled && defaultCliTool === tool) {
const tools = ['gemini', 'qwen', 'codex', 'claude'];
const newDefault = tools.find(t => {
if (t === tool) return false;
const status = cliToolStatus[t] || {};
const config = cliToolsConfig[t] || { enabled: true };
return status.available && config.enabled !== false;
});
if (newDefault) {
defaultCliTool = newDefault;
if (window.claudeCliToolsConfig) {
window.claudeCliToolsConfig.defaultTool = newDefault;
}
showRefreshToast(`Default tool switched to ${newDefault}`, 'info');
} else {
showRefreshToast(`Warning: No other enabled tool available for default`, 'warning');
}
}
await updateCliToolEnabled(tool, enabled);
await loadAllStatuses();
renderCliStatus();
}
function installCodexLens() {
openCodexLensInstallWizard();
}

View File

@@ -1389,7 +1389,13 @@ const i18n = {
'apiSettings.previewModel': 'Preview',
'apiSettings.modelSettings': 'Model Settings',
'apiSettings.deleteModel': 'Delete Model',
'apiSettings.endpointPreview': 'Endpoint Preview',
'apiSettings.modelBaseUrlOverride': 'Base URL Override',
'apiSettings.modelBaseUrlHint': 'Override the provider base URL for this specific model (leave empty to use provider default)',
'apiSettings.providerUpdated': 'Provider updated',
'apiSettings.syncToCodexLens': 'Sync to CodexLens',
'apiSettings.configSynced': 'Config synced to CodexLens',
'apiSettings.sdkAutoAppends': 'SDK auto-appends',
'apiSettings.preview': 'Preview',
'apiSettings.used': 'used',
'apiSettings.total': 'total',
@@ -1422,6 +1428,7 @@ const i18n = {
'apiSettings.cacheDisabled': 'Cache Disabled',
'apiSettings.providerSaved': 'Provider saved successfully',
'apiSettings.providerDeleted': 'Provider deleted successfully',
'apiSettings.apiBaseUpdated': 'API Base URL updated successfully',
'apiSettings.endpointSaved': 'Endpoint saved successfully',
'apiSettings.endpointDeleted': 'Endpoint deleted successfully',
'apiSettings.cacheCleared': 'Cache cleared successfully',
@@ -3039,7 +3046,12 @@ const i18n = {
'apiSettings.previewModel': '预览',
'apiSettings.modelSettings': '模型设置',
'apiSettings.deleteModel': '删除模型',
'apiSettings.endpointPreview': '端点预览',
'apiSettings.modelBaseUrlOverride': '基础 URL 覆盖',
'apiSettings.modelBaseUrlHint': '为此模型覆盖供应商的基础 URL留空则使用供应商默认值',
'apiSettings.providerUpdated': '供应商已更新',
'apiSettings.syncToCodexLens': '同步到 CodexLens',
'apiSettings.configSynced': '配置已同步到 CodexLens',
'apiSettings.preview': '预览',
'apiSettings.used': '已使用',
'apiSettings.total': '总计',
@@ -3072,6 +3084,7 @@ const i18n = {
'apiSettings.cacheDisabled': '缓存已禁用',
'apiSettings.providerSaved': '提供商保存成功',
'apiSettings.providerDeleted': '提供商删除成功',
'apiSettings.apiBaseUpdated': 'API 基础 URL 更新成功',
'apiSettings.endpointSaved': '端点保存成功',
'apiSettings.endpointDeleted': '端点删除成功',
'apiSettings.cacheCleared': '缓存清除成功',

View File

@@ -359,10 +359,20 @@ async function deleteProvider(providerId) {
/**
* Test provider connection
* @param {string} [providerIdParam] - Optional provider ID. If not provided, uses form context or selectedProviderId
*/
async function testProviderConnection() {
const form = document.getElementById('providerForm');
const providerId = form.dataset.providerId;
async function testProviderConnection(providerIdParam) {
var providerId = providerIdParam;
// Try to get providerId from different sources
if (!providerId) {
var form = document.getElementById('providerForm');
if (form && form.dataset.providerId) {
providerId = form.dataset.providerId;
} else if (selectedProviderId) {
providerId = selectedProviderId;
}
}
if (!providerId) {
showRefreshToast(t('apiSettings.saveProviderFirst'), 'warning');
@@ -553,9 +563,9 @@ async function showAddEndpointModal() {
'</div>' +
'</fieldset>' +
'<div class="modal-actions">' +
'<button type="button" class="btn btn-secondary" onclick="closeEndpointModal()">' + t('common.cancel') + '</button>' +
'<button type="button" class="btn btn-secondary" onclick="closeEndpointModal()"><i data-lucide="x"></i> ' + t('common.cancel') + '</button>' +
'<button type="submit" class="btn btn-primary">' +
'<i data-lucide="save"></i> ' + t('common.save') +
'<i data-lucide="check"></i> ' + t('common.save') +
'</button>' +
'</div>' +
'</form>' +
@@ -845,7 +855,10 @@ async function renderApiSettings() {
}
// Build split layout
container.innerHTML = '<div class="api-settings-container api-settings-split">' +
container.innerHTML =
// CCW-LiteLLM Status Container
'<div id="ccwLitellmStatusContainer" class="mb-4"></div>' +
'<div class="api-settings-container api-settings-split">' +
// Left Sidebar
'<aside class="api-settings-sidebar">' +
sidebarTabsHtml +
@@ -878,6 +891,9 @@ async function renderApiSettings() {
renderCacheMainPanel();
}
// Check and render ccw-litellm status
checkCcwLitellmStatus().then(renderCcwLitellmStatusCard);
if (window.lucide) lucide.createIcons();
}
@@ -966,7 +982,10 @@ function renderProviderDetail(providerId) {
}
var maskedKey = provider.apiKey ? '••••••••••••••••' + provider.apiKey.slice(-4) : '••••••••';
var apiBasePreview = (provider.apiBase || getDefaultApiBase(provider.type)) + '/chat/completions';
var currentApiBase = provider.apiBase || getDefaultApiBase(provider.type);
// Show full endpoint URL preview based on active model tab
var endpointPath = activeModelTab === 'embedding' ? '/embeddings' : '/chat/completions';
var apiBasePreview = currentApiBase + endpointPath;
var html = '<div class="provider-detail-header">' +
'<div class="provider-detail-title">' +
@@ -1007,13 +1026,18 @@ function renderProviderDetail(providerId) {
'<button class="btn btn-secondary" onclick="testProviderConnection()">' + t('apiSettings.testConnection') + '</button>' +
'</div>' +
'</div>' +
// API Base URL field
// API Base URL field - editable
'<div class="field-group">' +
'<div class="field-label">' +
'<span>' + t('apiSettings.apiBaseUrl') + '</span>' +
'</div>' +
'<input type="text" class="cli-input" value="' + escapeHtml(provider.apiBase || getDefaultApiBase(provider.type)) + '" readonly />' +
'<span class="field-hint">' + t('apiSettings.preview') + ': ' + apiBasePreview + '</span>' +
'<div class="field-input-group">' +
'<input type="text" class="cli-input" id="provider-detail-apibase" value="' + escapeHtml(currentApiBase) + '" placeholder="https://api.openai.com/v1" oninput="updateApiBasePreview(this.value)" />' +
'<button class="btn btn-secondary" onclick="saveProviderApiBase(\'' + providerId + '\')">' +
'<i data-lucide="save"></i> ' + t('common.save') +
'</button>' +
'</div>' +
'<span class="field-hint" id="api-base-preview">' + t('apiSettings.preview') + ': ' + escapeHtml(apiBasePreview) + '</span>' +
'</div>' +
// Model Section
'<div class="model-section">' +
@@ -1037,11 +1061,14 @@ function renderProviderDetail(providerId) {
'</div>' +
'<div class="model-tree" id="model-tree"></div>' +
'</div>' +
// Multi-key settings button
// Multi-key and sync buttons
'<div class="multi-key-trigger">' +
'<button class="btn btn-secondary multi-key-btn" onclick="showMultiKeyModal(\'' + providerId + '\')">' +
'<i data-lucide="key-round"></i> ' + t('apiSettings.multiKeySettings') +
'</button>' +
'<button class="btn btn-secondary" onclick="syncConfigToCodexLens()">' +
'<i data-lucide="refresh-cw"></i> ' + t('apiSettings.syncToCodexLens') +
'</button>' +
'</div>' +
'</div>';
@@ -1107,18 +1134,21 @@ function renderModelTree(provider) {
? formatContextWindow(model.capabilities.contextWindow)
: '';
// Badge for embedding models shows dimension instead of context window
var embeddingBadge = model.capabilities && model.capabilities.embeddingDimension
? model.capabilities.embeddingDimension + 'd'
: '';
var displayBadge = activeModelTab === 'llm' ? badge : embeddingBadge;
html += '<div class="model-item" data-model-id="' + model.id + '">' +
'<i data-lucide="' + (activeModelTab === 'llm' ? 'sparkles' : 'box') + '" class="model-item-icon"></i>' +
'<span class="model-item-name">' + escapeHtml(model.name) + '</span>' +
(badge ? '<span class="model-item-badge">' + badge + '</span>' : '') +
(displayBadge ? '<span class="model-item-badge">' + displayBadge + '</span>' : '') +
'<div class="model-item-actions">' +
'<button class="btn-icon-sm" onclick="previewModel(\'' + model.id + '\')" title="' + t('apiSettings.previewModel') + '">' +
'<i data-lucide="eye"></i>' +
'</button>' +
'<button class="btn-icon-sm" onclick="showModelSettingsModal(\'' + model.id + '\')" title="' + t('apiSettings.modelSettings') + '">' +
'<button class="btn-icon-sm" onclick="showModelSettingsModal(\'' + selectedProviderId + '\', \'' + model.id + '\', \'' + activeModelTab + '\')" title="' + t('apiSettings.modelSettings') + '">' +
'<i data-lucide="settings"></i>' +
'</button>' +
'<button class="btn-icon-sm text-destructive" onclick="deleteModel(\'' + model.id + '\')" title="' + t('apiSettings.deleteModel') + '">' +
'<button class="btn-icon-sm text-destructive" onclick="deleteModel(\'' + selectedProviderId + '\', \'' + model.id + '\', \'' + activeModelTab + '\')" title="' + t('apiSettings.deleteModel') + '">' +
'<i data-lucide="trash-2"></i>' +
'</button>' +
'</div>' +
@@ -1418,8 +1448,8 @@ function showAddModelModal(providerId, modelType) {
'</div>' +
'<div class="modal-actions">' +
'<button type="button" class="btn btn-secondary" onclick="closeAddModelModal()">' + t('common.cancel') + '</button>' +
'<button type="submit" class="btn btn-primary">' + t('common.save') + '</button>' +
'<button type="button" class="btn btn-secondary" onclick="closeAddModelModal()"><i data-lucide="x"></i> ' + t('common.cancel') + '</button>' +
'<button type="submit" class="btn btn-primary"><i data-lucide="check"></i> ' + t('common.save') + '</button>' +
'</div>' +
'</form>' +
'</div>' +
@@ -1624,29 +1654,51 @@ function showModelSettingsModal(providerId, modelId, modelType) {
var capabilities = model.capabilities || {};
var endpointSettings = model.endpointSettings || {};
// Calculate endpoint preview URL
var providerBase = provider.apiBase || getDefaultApiBase(provider.type);
var modelBaseUrl = endpointSettings.baseUrl || providerBase;
var endpointPath = isLlm ? '/chat/completions' : '/embeddings';
var endpointPreview = modelBaseUrl + endpointPath;
var modalHtml = '<div class="modal-overlay" id="model-settings-modal">' +
'<div class="modal-content" style="max-width: 550px;">' +
'<div class="modal-content" style="max-width: 600px;">' +
'<div class="modal-header">' +
'<h3>' + t('apiSettings.modelSettings') + ': ' + model.name + '</h3>' +
'<h3>' + t('apiSettings.modelSettings') + ': ' + escapeHtml(model.name) + '</h3>' +
'<button class="modal-close" onclick="closeModelSettingsModal()">&times;</button>' +
'</div>' +
'<div class="modal-body">' +
'<form id="model-settings-form" onsubmit="saveModelSettings(event, \'' + providerId + '\', \'' + modelId + '\', \'' + modelType + '\')">' +
// Endpoint Preview Section (combined view + settings)
'<div class="form-section endpoint-preview-section">' +
'<h4><i data-lucide="' + (isLlm ? 'message-square' : 'box') + '"></i> ' + t('apiSettings.endpointPreview') + '</h4>' +
'<div class="endpoint-preview-box">' +
'<code id="model-endpoint-preview">' + escapeHtml(endpointPreview) + '</code>' +
'<button type="button" class="btn-icon-sm" onclick="copyModelEndpoint()" title="' + t('common.copy') + '">' +
'<i data-lucide="copy"></i>' +
'</button>' +
'</div>' +
'<div class="form-group">' +
'<label>' + t('apiSettings.modelBaseUrlOverride') + ' <span class="text-muted">(' + t('common.optional') + ')</span></label>' +
'<input type="text" id="model-settings-baseurl" class="cli-input" value="' + escapeHtml(endpointSettings.baseUrl || '') + '" placeholder="' + escapeHtml(providerBase) + '" oninput="updateModelEndpointPreview(\'' + (isLlm ? 'chat/completions' : 'embeddings') + '\', \'' + escapeHtml(providerBase) + '\')">' +
'<small class="form-hint">' + t('apiSettings.modelBaseUrlHint') + '</small>' +
'</div>' +
'</div>' +
// Basic Info
'<div class="form-section">' +
'<h4>' + t('apiSettings.basicInfo') + '</h4>' +
'<div class="form-group">' +
'<label>' + t('apiSettings.modelName') + '</label>' +
'<input type="text" id="model-settings-name" class="cli-input" value="' + (model.name || '') + '" required>' +
'<input type="text" id="model-settings-name" class="cli-input" value="' + escapeHtml(model.name || '') + '" required>' +
'</div>' +
'<div class="form-group">' +
'<label>' + t('apiSettings.modelSeries') + '</label>' +
'<input type="text" id="model-settings-series" class="cli-input" value="' + (model.series || '') + '" required>' +
'<input type="text" id="model-settings-series" class="cli-input" value="' + escapeHtml(model.series || '') + '" required>' +
'</div>' +
'<div class="form-group">' +
'<label>' + t('apiSettings.description') + '</label>' +
'<textarea id="model-settings-description" class="cli-input" rows="2">' + (model.description || '') + '</textarea>' +
'<textarea id="model-settings-description" class="cli-input" rows="2">' + escapeHtml(model.description || '') + '</textarea>' +
'</div>' +
'</div>' +
@@ -1678,19 +1730,21 @@ function showModelSettingsModal(providerId, modelId, modelType) {
// Endpoint Settings
'<div class="form-section">' +
'<h4>' + t('apiSettings.endpointSettings') + '</h4>' +
'<div class="form-group">' +
'<div class="form-row">' +
'<div class="form-group form-group-half">' +
'<label>' + t('apiSettings.timeout') + ' (' + t('apiSettings.seconds') + ')</label>' +
'<input type="number" id="model-settings-timeout" class="cli-input" value="' + (endpointSettings.timeout || 300) + '" min="10" max="3600">' +
'</div>' +
'<div class="form-group">' +
'<div class="form-group form-group-half">' +
'<label>' + t('apiSettings.maxRetries') + '</label>' +
'<input type="number" id="model-settings-retries" class="cli-input" value="' + (endpointSettings.maxRetries || 3) + '" min="0" max="10">' +
'</div>' +
'</div>' +
'</div>' +
'<div class="modal-actions">' +
'<button type="button" class="btn-secondary" onclick="closeModelSettingsModal()">' + t('common.cancel') + '</button>' +
'<button type="submit" class="btn-primary">' + t('common.save') + '</button>' +
'<button type="button" class="btn-secondary" onclick="closeModelSettingsModal()"><i data-lucide="x"></i> ' + t('common.cancel') + '</button>' +
'<button type="submit" class="btn-primary"><i data-lucide="check"></i> ' + t('common.save') + '</button>' +
'</div>' +
'</form>' +
'</div>' +
@@ -1701,6 +1755,33 @@ function showModelSettingsModal(providerId, modelId, modelType) {
if (window.lucide) lucide.createIcons();
}
/**
* Update model endpoint preview when base URL changes
*/
function updateModelEndpointPreview(endpointPath, defaultBase) {
var baseUrlInput = document.getElementById('model-settings-baseurl');
var previewElement = document.getElementById('model-endpoint-preview');
if (!baseUrlInput || !previewElement) return;
var baseUrl = baseUrlInput.value.trim() || defaultBase;
// Remove trailing slash if present
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, -1);
}
previewElement.textContent = baseUrl + '/' + endpointPath;
}
/**
* Copy model endpoint URL to clipboard
*/
function copyModelEndpoint() {
var previewElement = document.getElementById('model-endpoint-preview');
if (previewElement) {
navigator.clipboard.writeText(previewElement.textContent);
showRefreshToast(t('common.copied'), 'success');
}
}
function closeModelSettingsModal() {
var modal = document.getElementById('model-settings-modal');
if (modal) modal.remove();
@@ -1744,7 +1825,13 @@ function saveModelSettings(event, providerId, modelId, modelType) {
}
// Update endpoint settings
var baseUrlOverride = document.getElementById('model-settings-baseurl').value.trim();
// Remove trailing slash if present
if (baseUrlOverride && baseUrlOverride.endsWith('/')) {
baseUrlOverride = baseUrlOverride.slice(0, -1);
}
models[modelIndex].endpointSettings = {
baseUrl: baseUrlOverride || undefined,
timeout: parseInt(document.getElementById('model-settings-timeout').value) || 300,
maxRetries: parseInt(document.getElementById('model-settings-retries').value) || 3
};
@@ -1774,11 +1861,6 @@ function saveModelSettings(event, providerId, modelId, modelType) {
});
}
function previewModel(providerId, modelId, modelType) {
// Just open the settings modal in read mode for now
showModelSettingsModal(providerId, modelId, modelType);
}
function deleteModel(providerId, modelId, modelType) {
if (!confirm(t('common.confirmDelete'))) return;
@@ -1823,6 +1905,59 @@ function copyProviderApiKey(providerId) {
}
}
/**
* Save provider API base URL
*/
async function saveProviderApiBase(providerId) {
var input = document.getElementById('provider-detail-apibase');
if (!input) return;
var newApiBase = input.value.trim();
// Remove trailing slash if present
if (newApiBase.endsWith('/')) {
newApiBase = newApiBase.slice(0, -1);
}
try {
var response = await fetch('/api/litellm-api/providers/' + providerId, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ apiBase: newApiBase || undefined })
});
if (!response.ok) throw new Error('Failed to update API base');
// Update local data
var provider = apiSettingsData.providers.find(function(p) { return p.id === providerId; });
if (provider) {
provider.apiBase = newApiBase || undefined;
}
// Update preview
updateApiBasePreview(newApiBase);
showRefreshToast(t('apiSettings.apiBaseUpdated'), 'success');
} catch (err) {
console.error('Failed to save API base:', err);
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
}
}
/**
* Update API base preview text showing full endpoint URL
*/
function updateApiBasePreview(apiBase) {
var preview = document.getElementById('api-base-preview');
if (!preview) return;
var base = apiBase || getDefaultApiBase('openai');
// Remove trailing slash if present
if (base.endsWith('/')) {
base = base.slice(0, -1);
}
var endpointPath = activeModelTab === 'embedding' ? '/embeddings' : '/chat/completions';
preview.textContent = t('apiSettings.preview') + ': ' + base + endpointPath;
}
/**
* Delete provider with confirmation
*/
@@ -1859,6 +1994,25 @@ async function deleteProviderWithConfirm(providerId) {
}
}
/**
* Sync config to CodexLens (generate YAML config for ccw_litellm)
*/
async function syncConfigToCodexLens() {
try {
var response = await fetch('/api/litellm-api/config/sync', {
method: 'POST'
});
if (!response.ok) throw new Error('Failed to sync config');
var result = await response.json();
showRefreshToast(t('apiSettings.configSynced') + ' (' + result.yamlPath + ')', 'success');
} catch (err) {
console.error('Failed to sync config:', err);
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
}
}
/**
* Get provider icon class based on type
*/
@@ -2343,7 +2497,7 @@ function showMultiKeyModal(providerId) {
renderHealthCheckSection(provider) +
'</div>' +
'<div class="modal-actions">' +
'<button type="button" class="btn-primary" onclick="closeMultiKeyModal()">' + t('common.close') + '</button>' +
'<button type="button" class="btn-primary" onclick="closeMultiKeyModal()"><i data-lucide="check"></i> ' + t('common.close') + '</button>' +
'</div>' +
'</div>' +
'</div>';
@@ -2578,6 +2732,99 @@ function toggleKeyVisibility(btn) {
}
// ========== CCW-LiteLLM Management ==========
/**
* Check ccw-litellm installation status
*/
async function checkCcwLitellmStatus() {
try {
var response = await fetch('/api/litellm-api/ccw-litellm/status');
var status = await response.json();
window.ccwLitellmStatus = status;
return status;
} catch (e) {
console.warn('[API Settings] Could not check ccw-litellm status:', e);
return { installed: false };
}
}
/**
* Render ccw-litellm status card
*/
function renderCcwLitellmStatusCard() {
var container = document.getElementById('ccwLitellmStatusContainer');
if (!container) return;
var status = window.ccwLitellmStatus || { installed: false };
if (status.installed) {
container.innerHTML =
'<div class="flex items-center gap-2 text-sm">' +
'<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-success/10 text-success border border-success/20">' +
'<i data-lucide="check-circle" class="w-3.5 h-3.5"></i>' +
'ccw-litellm ' + (status.version || '') +
'</span>' +
'</div>';
} else {
container.innerHTML =
'<div class="flex items-center gap-2">' +
'<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-muted text-muted-foreground border border-border text-sm">' +
'<i data-lucide="circle" class="w-3.5 h-3.5"></i>' +
'ccw-litellm not installed' +
'</span>' +
'<button class="btn-sm btn-primary" onclick="installCcwLitellm()">' +
'<i data-lucide="download" class="w-3.5 h-3.5"></i> Install' +
'</button>' +
'</div>';
}
if (window.lucide) lucide.createIcons();
}
/**
* Install ccw-litellm package
*/
async function installCcwLitellm() {
var container = document.getElementById('ccwLitellmStatusContainer');
if (container) {
container.innerHTML =
'<div class="flex items-center gap-2 text-sm text-muted-foreground">' +
'<div class="animate-spin w-4 h-4 border-2 border-primary border-t-transparent rounded-full"></div>' +
'Installing ccw-litellm...' +
'</div>';
}
try {
var response = await fetch('/api/litellm-api/ccw-litellm/install', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
});
var result = await response.json();
if (result.success) {
showRefreshToast('ccw-litellm installed successfully!', 'success');
// Refresh status
await checkCcwLitellmStatus();
renderCcwLitellmStatusCard();
} else {
showRefreshToast('Failed to install ccw-litellm: ' + result.error, 'error');
renderCcwLitellmStatusCard();
}
} catch (e) {
showRefreshToast('Installation error: ' + e.message, 'error');
renderCcwLitellmStatusCard();
}
}
// Make functions globally accessible
window.checkCcwLitellmStatus = checkCcwLitellmStatus;
window.renderCcwLitellmStatusCard = renderCcwLitellmStatusCard;
window.installCcwLitellm = installCcwLitellm;
// ========== Utility Functions ==========
/**

View File

@@ -1166,10 +1166,12 @@ async function deleteModel(profile) {
* Initialize CodexLens index with bottom floating progress bar
* @param {string} indexType - 'vector' (with embeddings), 'normal' (FTS only), or 'full' (FTS + Vector)
* @param {string} embeddingModel - Model profile: 'code', 'fast'
* @param {string} embeddingBackend - Backend: 'fastembed' (local) or 'litellm' (API)
*/
async function initCodexLensIndex(indexType, embeddingModel) {
async function initCodexLensIndex(indexType, embeddingModel, embeddingBackend) {
indexType = indexType || 'vector';
embeddingModel = embeddingModel || 'code';
embeddingBackend = embeddingBackend || 'fastembed';
// For vector or full index, check if semantic dependencies are available
if (indexType === 'vector' || indexType === 'full') {
@@ -1235,7 +1237,8 @@ async function initCodexLensIndex(indexType, embeddingModel) {
var modelLabel = '';
if (indexType !== 'normal') {
var modelNames = { code: 'Code', fast: 'Fast' };
modelLabel = ' [' + (modelNames[embeddingModel] || embeddingModel) + ']';
var backendLabel = embeddingBackend === 'litellm' ? 'API: ' : '';
modelLabel = ' [' + backendLabel + (modelNames[embeddingModel] || embeddingModel) + ']';
}
progressBar.innerHTML =
@@ -1272,17 +1275,19 @@ async function initCodexLensIndex(indexType, embeddingModel) {
var apiIndexType = (indexType === 'full') ? 'vector' : indexType;
// Start indexing with specified type and model
startCodexLensIndexing(apiIndexType, embeddingModel);
startCodexLensIndexing(apiIndexType, embeddingModel, embeddingBackend);
}
/**
* Start the indexing process
* @param {string} indexType - 'vector' or 'normal'
* @param {string} embeddingModel - Model profile: 'code', 'fast'
* @param {string} embeddingBackend - Backend: 'fastembed' (local) or 'litellm' (API)
*/
async function startCodexLensIndexing(indexType, embeddingModel) {
async function startCodexLensIndexing(indexType, embeddingModel, embeddingBackend) {
indexType = indexType || 'vector';
embeddingModel = embeddingModel || 'code';
embeddingBackend = embeddingBackend || 'fastembed';
var statusText = document.getElementById('codexlensIndexStatus');
var progressBar = document.getElementById('codexlensIndexProgressBar');
var percentText = document.getElementById('codexlensIndexPercent');
@@ -1314,11 +1319,11 @@ async function startCodexLensIndexing(indexType, embeddingModel) {
}
try {
console.log('[CodexLens] Starting index for:', projectPath, 'type:', indexType, 'model:', embeddingModel);
console.log('[CodexLens] Starting index for:', projectPath, 'type:', indexType, 'model:', embeddingModel, 'backend:', embeddingBackend);
var response = await fetch('/api/codexlens/init', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: projectPath, indexType: indexType, embeddingModel: embeddingModel })
body: JSON.stringify({ path: projectPath, indexType: indexType, embeddingModel: embeddingModel, embeddingBackend: embeddingBackend })
});
var result = await response.json();
@@ -1883,6 +1888,16 @@ async function renderCodexLensManager() {
await loadCodexLensStatus();
}
// Load LiteLLM API config for embedding backend options
try {
var litellmResponse = await fetch('/api/litellm-api/config');
if (litellmResponse.ok) {
window.litellmApiConfig = await litellmResponse.json();
}
} catch (e) {
console.warn('[CodexLens] Could not load LiteLLM config:', e);
}
var response = await fetch('/api/codexlens/config');
var config = await response.json();
@@ -1946,6 +1961,15 @@ function buildCodexLensManagerPage(config) {
'<div class="bg-card border border-border rounded-lg p-5">' +
'<h4 class="text-lg font-semibold mb-4 flex items-center gap-2"><i data-lucide="layers" class="w-5 h-5 text-primary"></i> ' + t('codexlens.createIndex') + '</h4>' +
'<div class="space-y-4">' +
// Backend selector (fastembed local or litellm API)
'<div class="mb-4">' +
'<label class="block text-sm font-medium mb-1.5">' + (t('codexlens.embeddingBackend') || 'Embedding Backend') + '</label>' +
'<select id="pageBackendSelect" class="w-full px-3 py-2 border border-border rounded-lg bg-background text-sm" onchange="onEmbeddingBackendChange()">' +
'<option value="fastembed">' + (t('codexlens.localFastembed') || 'Local (FastEmbed)') + '</option>' +
'<option value="litellm">' + (t('codexlens.apiLitellm') || 'API (LiteLLM)') + '</option>' +
'</select>' +
'<p class="text-xs text-muted-foreground mt-1">' + (t('codexlens.backendHint') || 'Select local model or remote API endpoint') + '</p>' +
'</div>' +
// Model selector
'<div>' +
'<label class="block text-sm font-medium mb-1.5">' + t('codexlens.embeddingModel') + '</label>' +
@@ -2150,18 +2174,68 @@ function buildModelSelectOptionsForPage() {
return options;
}
/**
* Handle embedding backend change
*/
function onEmbeddingBackendChange() {
var backendSelect = document.getElementById('pageBackendSelect');
var modelSelect = document.getElementById('pageModelSelect');
if (!backendSelect || !modelSelect) return;
var backend = backendSelect.value;
if (backend === 'litellm') {
// Load LiteLLM embedding models
modelSelect.innerHTML = buildLiteLLMModelOptions();
} else {
// Load local fastembed models
modelSelect.innerHTML = buildModelSelectOptionsForPage();
}
}
/**
* Build LiteLLM model options from config
*/
function buildLiteLLMModelOptions() {
var litellmConfig = window.litellmApiConfig || {};
var providers = litellmConfig.providers || [];
var options = '';
providers.forEach(function(provider) {
if (!provider.enabled) return;
var models = provider.models || [];
models.forEach(function(model) {
if (model.type !== 'embedding' || !model.enabled) return;
var label = model.name || model.id;
var selected = options === '' ? ' selected' : '';
options += '<option value="' + model.id + '"' + selected + '>' + label + '</option>';
});
});
if (options === '') {
options = '<option value="" disabled selected>' + (t('codexlens.noApiModels') || 'No API embedding models configured') + '</option>';
}
return options;
}
// Make functions globally accessible
window.onEmbeddingBackendChange = onEmbeddingBackendChange;
/**
* Initialize index from page with selected model
*/
function initCodexLensIndexFromPage(indexType) {
var backendSelect = document.getElementById('pageBackendSelect');
var modelSelect = document.getElementById('pageModelSelect');
var selectedBackend = backendSelect ? backendSelect.value : 'fastembed';
var selectedModel = modelSelect ? modelSelect.value : 'code';
// For FTS-only index, model is not needed
if (indexType === 'normal') {
initCodexLensIndex(indexType);
} else {
initCodexLensIndex(indexType, selectedModel);
initCodexLensIndex(indexType, selectedModel, selectedBackend);
}
}