mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-12 02:37:45 +08:00
feat: Add reranker model management commands and UI integration
- Implemented CLI commands for listing, downloading, deleting, and retrieving information about reranker models. - Enhanced the dashboard UI to support embedding and reranker configurations with internationalization. - Updated environment variable management for embedding and reranker settings. - Added functionality to dynamically update model options based on selected backend. - Improved user experience with status indicators and action buttons for model management. - Integrated new reranker models with detailed metadata and recommendations.
This commit is contained in:
@@ -1252,7 +1252,7 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
|
|||||||
const { backend, model_name, api_provider, api_key, litellm_endpoint } = body;
|
const { backend, model_name, api_provider, api_key, litellm_endpoint } = body;
|
||||||
|
|
||||||
// Validate backend
|
// Validate backend
|
||||||
const validBackends = ['onnx', 'api', 'litellm', 'legacy'];
|
const validBackends = ['onnx', 'api', 'litellm', 'legacy', 'fastembed'];
|
||||||
if (backend && !validBackends.includes(backend)) {
|
if (backend && !validBackends.includes(backend)) {
|
||||||
return { success: false, error: `Invalid backend: ${backend}. Valid options: ${validBackends.join(', ')}`, status: 400 };
|
return { success: false, error: `Invalid backend: ${backend}. Valid options: ${validBackends.join(', ')}`, status: 400 };
|
||||||
}
|
}
|
||||||
@@ -1310,6 +1310,129 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// RERANKER MODEL MANAGEMENT ENDPOINTS
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// API: List Reranker Models (list available reranker models)
|
||||||
|
if (pathname === '/api/codexlens/reranker/models' && req.method === 'GET') {
|
||||||
|
try {
|
||||||
|
// Check if CodexLens is installed first
|
||||||
|
const venvStatus = await checkVenvStatus();
|
||||||
|
if (!venvStatus.ready) {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ success: false, error: 'CodexLens not installed' }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const result = await executeCodexLens(['reranker-model-list', '--json']);
|
||||||
|
if (result.success) {
|
||||||
|
try {
|
||||||
|
const parsed = extractJSON(result.output);
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify(parsed));
|
||||||
|
} catch {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ success: true, result: { models: [] }, output: result.output }));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ success: false, error: result.error }));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ success: false, error: err.message }));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API: Download Reranker Model (download reranker model by profile)
|
||||||
|
if (pathname === '/api/codexlens/reranker/models/download' && req.method === 'POST') {
|
||||||
|
handlePostRequest(req, res, async (body) => {
|
||||||
|
const { profile } = body;
|
||||||
|
|
||||||
|
if (!profile) {
|
||||||
|
return { success: false, error: 'profile is required', status: 400 };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await executeCodexLens(['reranker-model-download', profile, '--json'], { timeout: 600000 }); // 10 min for download
|
||||||
|
if (result.success) {
|
||||||
|
try {
|
||||||
|
const parsed = extractJSON(result.output);
|
||||||
|
return { success: true, ...parsed };
|
||||||
|
} catch {
|
||||||
|
return { success: true, output: result.output };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return { success: false, error: result.error, status: 500 };
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return { success: false, error: err.message, status: 500 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API: Delete Reranker Model (delete reranker model by profile)
|
||||||
|
if (pathname === '/api/codexlens/reranker/models/delete' && req.method === 'POST') {
|
||||||
|
handlePostRequest(req, res, async (body) => {
|
||||||
|
const { profile } = body;
|
||||||
|
|
||||||
|
if (!profile) {
|
||||||
|
return { success: false, error: 'profile is required', status: 400 };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await executeCodexLens(['reranker-model-delete', profile, '--json']);
|
||||||
|
if (result.success) {
|
||||||
|
try {
|
||||||
|
const parsed = extractJSON(result.output);
|
||||||
|
return { success: true, ...parsed };
|
||||||
|
} catch {
|
||||||
|
return { success: true, output: result.output };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return { success: false, error: result.error, status: 500 };
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return { success: false, error: err.message, status: 500 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API: Reranker Model Info (get reranker model info by profile)
|
||||||
|
if (pathname === '/api/codexlens/reranker/models/info' && req.method === 'GET') {
|
||||||
|
const profile = url.searchParams.get('profile');
|
||||||
|
|
||||||
|
if (!profile) {
|
||||||
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ success: false, error: 'profile parameter is required' }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await executeCodexLens(['reranker-model-info', profile, '--json']);
|
||||||
|
if (result.success) {
|
||||||
|
try {
|
||||||
|
const parsed = extractJSON(result.output);
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify(parsed));
|
||||||
|
} catch {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ success: false, error: 'Failed to parse response' }));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ success: false, error: result.error }));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ success: false, error: err.message }));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// FILE WATCHER CONTROL ENDPOINTS
|
// FILE WATCHER CONTROL ENDPOINTS
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
@@ -283,6 +283,22 @@ const i18n = {
|
|||||||
'codexlens.indexManagement': 'Management',
|
'codexlens.indexManagement': 'Management',
|
||||||
'codexlens.incrementalUpdate': 'Incremental Update',
|
'codexlens.incrementalUpdate': 'Incremental Update',
|
||||||
'codexlens.environmentVariables': 'Environment Variables',
|
'codexlens.environmentVariables': 'Environment Variables',
|
||||||
|
'codexlens.envGroup.embedding': 'Embedding Configuration',
|
||||||
|
'codexlens.envGroup.reranker': 'Reranker Configuration',
|
||||||
|
'codexlens.envGroup.concurrency': 'Concurrency Settings',
|
||||||
|
'codexlens.envGroup.cascade': 'Cascade Search Settings',
|
||||||
|
'codexlens.envGroup.llm': 'LLM Features',
|
||||||
|
'codexlens.usingApiReranker': 'Using API Reranker',
|
||||||
|
'codexlens.currentModel': 'Current Model',
|
||||||
|
'codexlens.localModels': 'Local Models',
|
||||||
|
'codexlens.active': 'Active',
|
||||||
|
'codexlens.useLocal': 'Use Local',
|
||||||
|
'codexlens.select': 'Select',
|
||||||
|
'codexlens.switchedToLocal': 'Switched to local',
|
||||||
|
'codexlens.configuredInApiSettings': 'Configured in API Settings',
|
||||||
|
'codexlens.commonModels': 'Common Models',
|
||||||
|
'codexlens.selectApiModel': 'Select API model...',
|
||||||
|
'codexlens.autoDownloadHint': 'Models are auto-downloaded on first use',
|
||||||
'codexlens.embeddingBackend': 'Embedding Backend',
|
'codexlens.embeddingBackend': 'Embedding Backend',
|
||||||
'codexlens.localFastembed': 'Local (FastEmbed)',
|
'codexlens.localFastembed': 'Local (FastEmbed)',
|
||||||
'codexlens.apiLitellm': 'API (LiteLLM)',
|
'codexlens.apiLitellm': 'API (LiteLLM)',
|
||||||
@@ -2248,6 +2264,22 @@ const i18n = {
|
|||||||
'codexlens.indexManagement': '管理',
|
'codexlens.indexManagement': '管理',
|
||||||
'codexlens.incrementalUpdate': '增量更新',
|
'codexlens.incrementalUpdate': '增量更新',
|
||||||
'codexlens.environmentVariables': '环境变量',
|
'codexlens.environmentVariables': '环境变量',
|
||||||
|
'codexlens.envGroup.embedding': '嵌入配置',
|
||||||
|
'codexlens.envGroup.reranker': '重排序配置',
|
||||||
|
'codexlens.envGroup.concurrency': '并发设置',
|
||||||
|
'codexlens.envGroup.cascade': '级联搜索设置',
|
||||||
|
'codexlens.envGroup.llm': 'LLM 功能',
|
||||||
|
'codexlens.usingApiReranker': '使用 API 重排序',
|
||||||
|
'codexlens.currentModel': '当前模型',
|
||||||
|
'codexlens.localModels': '本地模型',
|
||||||
|
'codexlens.active': '已激活',
|
||||||
|
'codexlens.useLocal': '切换本地',
|
||||||
|
'codexlens.select': '选择',
|
||||||
|
'codexlens.switchedToLocal': '已切换到本地',
|
||||||
|
'codexlens.configuredInApiSettings': '已在 API 设置中配置',
|
||||||
|
'codexlens.commonModels': '常用模型',
|
||||||
|
'codexlens.selectApiModel': '选择 API 模型...',
|
||||||
|
'codexlens.autoDownloadHint': '模型会在首次使用时自动下载',
|
||||||
'codexlens.embeddingBackend': '嵌入后端',
|
'codexlens.embeddingBackend': '嵌入后端',
|
||||||
'codexlens.localFastembed': '本地 (FastEmbed)',
|
'codexlens.localFastembed': '本地 (FastEmbed)',
|
||||||
'codexlens.apiLitellm': 'API (LiteLLM)',
|
'codexlens.apiLitellm': 'API (LiteLLM)',
|
||||||
|
|||||||
@@ -660,7 +660,7 @@ window.getModelLockState = getModelLockState;
|
|||||||
// Embedding and Reranker are configured separately
|
// Embedding and Reranker are configured separately
|
||||||
var ENV_VAR_GROUPS = {
|
var ENV_VAR_GROUPS = {
|
||||||
embedding: {
|
embedding: {
|
||||||
label: 'Embedding Configuration',
|
labelKey: 'codexlens.envGroup.embedding',
|
||||||
icon: 'box',
|
icon: 'box',
|
||||||
vars: {
|
vars: {
|
||||||
'CODEXLENS_EMBEDDING_BACKEND': { label: 'Backend', type: 'select', options: ['fastembed', 'litellm'], default: 'fastembed', settingsPath: 'embedding.backend' },
|
'CODEXLENS_EMBEDDING_BACKEND': { label: 'Backend', type: 'select', options: ['fastembed', 'litellm'], default: 'fastembed', settingsPath: 'embedding.backend' },
|
||||||
@@ -687,7 +687,7 @@ var ENV_VAR_GROUPS = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
reranker: {
|
reranker: {
|
||||||
label: 'Reranker Configuration',
|
labelKey: 'codexlens.envGroup.reranker',
|
||||||
icon: 'arrow-up-down',
|
icon: 'arrow-up-down',
|
||||||
vars: {
|
vars: {
|
||||||
'CODEXLENS_RERANKER_ENABLED': { label: 'Enabled', type: 'select', options: ['true', 'false'], default: 'true', settingsPath: 'reranker.enabled' },
|
'CODEXLENS_RERANKER_ENABLED': { label: 'Enabled', type: 'select', options: ['true', 'false'], default: 'true', settingsPath: 'reranker.enabled' },
|
||||||
@@ -711,17 +711,8 @@ var ENV_VAR_GROUPS = {
|
|||||||
'CODEXLENS_RERANKER_TOP_K': { label: 'Top K Results', type: 'number', placeholder: '50', default: '50', settingsPath: 'reranker.top_k', min: 5, max: 200 }
|
'CODEXLENS_RERANKER_TOP_K': { label: 'Top K Results', type: 'number', placeholder: '50', default: '50', settingsPath: 'reranker.top_k', min: 5, max: 200 }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
apiCredentials: {
|
|
||||||
label: 'API Credentials',
|
|
||||||
icon: 'key',
|
|
||||||
showWhen: function(env) { return env['CODEXLENS_EMBEDDING_BACKEND'] === 'litellm' || env['CODEXLENS_RERANKER_BACKEND'] === 'litellm' || env['CODEXLENS_RERANKER_BACKEND'] === 'api'; },
|
|
||||||
vars: {
|
|
||||||
'LITELLM_API_KEY': { label: 'API Key', placeholder: 'sk-...', type: 'password' },
|
|
||||||
'LITELLM_API_BASE': { label: 'API Base URL', placeholder: 'https://api.openai.com/v1' }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
concurrency: {
|
concurrency: {
|
||||||
label: 'Concurrency Settings',
|
labelKey: 'codexlens.envGroup.concurrency',
|
||||||
icon: 'cpu',
|
icon: 'cpu',
|
||||||
vars: {
|
vars: {
|
||||||
'CODEXLENS_API_MAX_WORKERS': { label: 'Max Workers', type: 'number', placeholder: '4', default: '4', settingsPath: 'api.max_workers', min: 1, max: 32 },
|
'CODEXLENS_API_MAX_WORKERS': { label: 'Max Workers', type: 'number', placeholder: '4', default: '4', settingsPath: 'api.max_workers', min: 1, max: 32 },
|
||||||
@@ -729,7 +720,7 @@ var ENV_VAR_GROUPS = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
cascade: {
|
cascade: {
|
||||||
label: 'Cascade Search Settings',
|
labelKey: 'codexlens.envGroup.cascade',
|
||||||
icon: 'git-branch',
|
icon: 'git-branch',
|
||||||
vars: {
|
vars: {
|
||||||
'CODEXLENS_CASCADE_STRATEGY': { label: 'Search Strategy', type: 'select', options: ['binary', 'hybrid', 'binary_rerank', 'dense_rerank'], default: 'dense_rerank', settingsPath: 'cascade.strategy' },
|
'CODEXLENS_CASCADE_STRATEGY': { label: 'Search Strategy', type: 'select', options: ['binary', 'hybrid', 'binary_rerank', 'dense_rerank'], default: 'dense_rerank', settingsPath: 'cascade.strategy' },
|
||||||
@@ -738,7 +729,7 @@ var ENV_VAR_GROUPS = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
llm: {
|
llm: {
|
||||||
label: 'LLM Features',
|
labelKey: 'codexlens.envGroup.llm',
|
||||||
icon: 'sparkles',
|
icon: 'sparkles',
|
||||||
collapsed: true,
|
collapsed: true,
|
||||||
vars: {
|
vars: {
|
||||||
@@ -802,10 +793,11 @@ async function loadEnvVariables() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var groupLabel = group.labelKey ? t(group.labelKey) : group.label;
|
||||||
html += '<div class="border border-border rounded-lg p-3">' +
|
html += '<div class="border border-border rounded-lg p-3">' +
|
||||||
'<div class="flex items-center gap-2 mb-2 text-xs font-medium text-muted-foreground">' +
|
'<div class="flex items-center gap-2 mb-2 text-xs font-medium text-muted-foreground">' +
|
||||||
'<i data-lucide="' + group.icon + '" class="w-3.5 h-3.5"></i>' +
|
'<i data-lucide="' + group.icon + '" class="w-3.5 h-3.5"></i>' +
|
||||||
group.label +
|
groupLabel +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="space-y-2">';
|
'<div class="space-y-2">';
|
||||||
|
|
||||||
@@ -927,14 +919,84 @@ async function loadEnvVariables() {
|
|||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
if (window.lucide) lucide.createIcons();
|
if (window.lucide) lucide.createIcons();
|
||||||
|
|
||||||
// Add change handler for backend selects to refresh display
|
// Add change handler for backend selects to dynamically update model options
|
||||||
var backendSelects = container.querySelectorAll('select[data-env-key*="BACKEND"]');
|
var backendSelects = container.querySelectorAll('select[data-env-key*="BACKEND"]');
|
||||||
backendSelects.forEach(function(select) {
|
backendSelects.forEach(function(select) {
|
||||||
select.addEventListener('change', function() {
|
select.addEventListener('change', function() {
|
||||||
// Trigger a re-render after saving
|
var backendKey = select.getAttribute('data-env-key');
|
||||||
|
var newBackend = select.value;
|
||||||
|
var isApiBackend = newBackend === 'litellm' || newBackend === 'api';
|
||||||
|
|
||||||
|
// Determine which model input to update
|
||||||
|
var isEmbedding = backendKey.indexOf('EMBEDDING') !== -1;
|
||||||
|
var modelKey = isEmbedding ? 'CODEXLENS_EMBEDDING_MODEL' : 'CODEXLENS_RERANKER_MODEL';
|
||||||
|
var modelInput = document.querySelector('[data-env-key="' + modelKey + '"]');
|
||||||
|
|
||||||
|
if (modelInput) {
|
||||||
|
var datalistId = modelInput.getAttribute('list');
|
||||||
|
var datalist = document.getElementById(datalistId);
|
||||||
|
|
||||||
|
if (datalist) {
|
||||||
|
// Get model config from ENV_VAR_GROUPS
|
||||||
|
var groupKey = isEmbedding ? 'embedding' : 'reranker';
|
||||||
|
var modelConfig = ENV_VAR_GROUPS[groupKey]?.vars[modelKey];
|
||||||
|
|
||||||
|
if (modelConfig) {
|
||||||
|
var modelList = isApiBackend
|
||||||
|
? (modelConfig.apiModels || modelConfig.models || [])
|
||||||
|
: (modelConfig.localModels || modelConfig.models || []);
|
||||||
|
var configuredModels = isEmbedding ? configuredEmbeddingModels : configuredRerankerModels;
|
||||||
|
|
||||||
|
// Rebuild datalist
|
||||||
|
var html = '';
|
||||||
|
if (isApiBackend && configuredModels.length > 0) {
|
||||||
|
html += '<option value="" disabled>-- ' + t('codexlens.configuredInApiSettings') + ' --</option>';
|
||||||
|
configuredModels.forEach(function(model) {
|
||||||
|
var providers = model.providers ? model.providers.join(', ') : '';
|
||||||
|
html += '<option value="' + escapeHtml(model.modelId) + '">' +
|
||||||
|
escapeHtml(model.modelName || model.modelId) +
|
||||||
|
(providers ? ' (' + escapeHtml(providers) + ')' : '') +
|
||||||
|
'</option>';
|
||||||
|
});
|
||||||
|
if (modelList.length > 0) {
|
||||||
|
html += '<option value="" disabled>-- ' + t('codexlens.commonModels') + ' --</option>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modelList.forEach(function(group) {
|
||||||
|
group.items.forEach(function(model) {
|
||||||
|
var exists = configuredModels.some(function(m) { return m.modelId === model; });
|
||||||
|
if (!exists) {
|
||||||
|
html += '<option value="' + escapeHtml(model) + '">' + escapeHtml(group.group) + ': ' + escapeHtml(model) + '</option>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
datalist.innerHTML = html;
|
||||||
|
|
||||||
|
// Clear current model value if it doesn't match new backend type
|
||||||
|
var currentValue = modelInput.value;
|
||||||
|
var isCurrentLocal = modelConfig.localModels?.some(function(g) {
|
||||||
|
return g.items.includes(currentValue);
|
||||||
|
});
|
||||||
|
var isCurrentApi = modelConfig.apiModels?.some(function(g) {
|
||||||
|
return g.items.includes(currentValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
// If switching to API and current is local (or vice versa), clear or set default
|
||||||
|
if (isApiBackend && isCurrentLocal) {
|
||||||
|
modelInput.value = '';
|
||||||
|
modelInput.placeholder = t('codexlens.selectApiModel');
|
||||||
|
} else if (!isApiBackend && isCurrentApi) {
|
||||||
|
modelInput.value = modelConfig.localModels?.[0]?.items?.[0] || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save and refresh after a short delay
|
||||||
saveEnvVariables().then(function() {
|
saveEnvVariables().then(function() {
|
||||||
loadEnvVariables();
|
loadEnvVariables();
|
||||||
// Refresh model lists to reflect new backend
|
|
||||||
loadModelList();
|
loadModelList();
|
||||||
loadRerankerModelList();
|
loadRerankerModelList();
|
||||||
});
|
});
|
||||||
@@ -947,6 +1009,7 @@ async function loadEnvVariables() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply LiteLLM provider settings to environment variables
|
* Apply LiteLLM provider settings to environment variables
|
||||||
|
* Note: API credentials are now managed via API Settings page
|
||||||
*/
|
*/
|
||||||
function applyLiteLLMProvider(providerId) {
|
function applyLiteLLMProvider(providerId) {
|
||||||
if (!providerId) return;
|
if (!providerId) return;
|
||||||
@@ -961,19 +1024,9 @@ function applyLiteLLMProvider(providerId) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-fill fields
|
// Auto-fill model fields based on provider
|
||||||
var apiKeyInput = document.querySelector('[data-env-key="LITELLM_API_KEY"]');
|
var embeddingModelInput = document.querySelector('[data-env-key="CODEXLENS_EMBEDDING_MODEL"]');
|
||||||
var apiBaseInput = document.querySelector('[data-env-key="LITELLM_API_BASE"]');
|
var rerankerModelInput = document.querySelector('[data-env-key="CODEXLENS_RERANKER_MODEL"]');
|
||||||
var embeddingModelInput = document.querySelector('[data-env-key="LITELLM_EMBEDDING_MODEL"]');
|
|
||||||
var rerankerModelInput = document.querySelector('[data-env-key="LITELLM_RERANKER_MODEL"]');
|
|
||||||
|
|
||||||
if (apiKeyInput && provider.api_key) {
|
|
||||||
apiKeyInput.value = provider.api_key;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (apiBaseInput && provider.api_base) {
|
|
||||||
apiBaseInput.value = provider.api_base;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set default models based on provider type
|
// Set default models based on provider type
|
||||||
var providerName = (provider.name || provider.id || '').toLowerCase();
|
var providerName = (provider.name || provider.id || '').toLowerCase();
|
||||||
@@ -1706,17 +1759,18 @@ async function deleteModel(profile) {
|
|||||||
// RERANKER MODEL MANAGEMENT
|
// RERANKER MODEL MANAGEMENT
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
// Available reranker models (fastembed TextCrossEncoder)
|
// Available reranker models (fastembed TextCrossEncoder) - fallback if API unavailable
|
||||||
var RERANKER_MODELS = [
|
var RERANKER_MODELS = [
|
||||||
{ id: 'ms-marco-mini', name: 'Xenova/ms-marco-MiniLM-L-6-v2', size: 80, desc: 'Fast, lightweight' },
|
{ id: 'ms-marco-mini', name: 'Xenova/ms-marco-MiniLM-L-6-v2', size: 90, desc: 'Fast, lightweight', recommended: true },
|
||||||
{ id: 'ms-marco-12', name: 'Xenova/ms-marco-MiniLM-L-12-v2', size: 120, desc: 'Better accuracy' },
|
{ id: 'ms-marco-12', name: 'Xenova/ms-marco-MiniLM-L-12-v2', size: 130, desc: 'Better accuracy', recommended: true },
|
||||||
{ id: 'bge-base', name: 'BAAI/bge-reranker-base', size: 1040, desc: 'High quality' },
|
{ id: 'bge-base', name: 'BAAI/bge-reranker-base', size: 280, desc: 'High quality', recommended: true },
|
||||||
{ id: 'jina-tiny', name: 'jinaai/jina-reranker-v1-tiny-en', size: 130, desc: 'Tiny, fast' },
|
{ id: 'bge-large', name: 'BAAI/bge-reranker-large', size: 560, desc: 'Maximum quality', recommended: false },
|
||||||
{ id: 'jina-turbo', name: 'jinaai/jina-reranker-v1-turbo-en', size: 150, desc: 'Balanced' }
|
{ id: 'jina-tiny', name: 'jinaai/jina-reranker-v1-tiny-en', size: 70, desc: 'Tiny, fast', recommended: true },
|
||||||
|
{ id: 'jina-turbo', name: 'jinaai/jina-reranker-v1-turbo-en', size: 150, desc: 'Balanced', recommended: true }
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load reranker model list
|
* Load reranker model list with download/delete support
|
||||||
*/
|
*/
|
||||||
async function loadRerankerModelList() {
|
async function loadRerankerModelList() {
|
||||||
// Update both containers (advanced tab and page model management)
|
// Update both containers (advanced tab and page model management)
|
||||||
@@ -1725,25 +1779,57 @@ async function loadRerankerModelList() {
|
|||||||
document.getElementById('pageRerankerModelListContainer')
|
document.getElementById('pageRerankerModelListContainer')
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
if (containers.length === 0) return;
|
console.log('[CodexLens] loadRerankerModelList - containers found:', containers.length);
|
||||||
|
|
||||||
|
if (containers.length === 0) {
|
||||||
|
console.warn('[CodexLens] No reranker model list containers found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get current reranker config
|
// Fetch both config and models list in parallel
|
||||||
var response = await fetch('/api/codexlens/reranker/config');
|
var [configResponse, modelsResponse] = await Promise.all([
|
||||||
if (!response.ok) {
|
fetch('/api/codexlens/reranker/config'),
|
||||||
throw new Error('Failed to load reranker config: ' + response.status);
|
fetch('/api/codexlens/reranker/models')
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!configResponse.ok) {
|
||||||
|
throw new Error('Failed to load reranker config: ' + configResponse.status);
|
||||||
}
|
}
|
||||||
var config = await response.json();
|
var config = await configResponse.json();
|
||||||
|
console.log('[CodexLens] Reranker config loaded:', { backend: config.backend, model: config.model_name });
|
||||||
|
|
||||||
// Handle API response format
|
// Handle API response format
|
||||||
var currentModel = config.model_name || config.result?.reranker_model || 'Xenova/ms-marco-MiniLM-L-6-v2';
|
var currentModel = config.model_name || config.result?.reranker_model || 'Xenova/ms-marco-MiniLM-L-6-v2';
|
||||||
var currentBackend = config.backend || config.result?.reranker_backend || 'fastembed';
|
var currentBackend = config.backend || config.result?.reranker_backend || 'fastembed';
|
||||||
|
|
||||||
|
// Try to use API models, fall back to static list
|
||||||
|
var models = RERANKER_MODELS;
|
||||||
|
var modelsFromApi = false;
|
||||||
|
if (modelsResponse.ok) {
|
||||||
|
var modelsData = await modelsResponse.json();
|
||||||
|
if (modelsData.success && modelsData.result && modelsData.result.models) {
|
||||||
|
models = modelsData.result.models.map(function(m) {
|
||||||
|
return {
|
||||||
|
id: m.profile,
|
||||||
|
name: m.model_name,
|
||||||
|
size: m.installed && m.actual_size_mb ? m.actual_size_mb : m.estimated_size_mb,
|
||||||
|
desc: m.description,
|
||||||
|
installed: m.installed,
|
||||||
|
recommended: m.recommended
|
||||||
|
};
|
||||||
|
});
|
||||||
|
modelsFromApi = true;
|
||||||
|
console.log('[CodexLens] Loaded ' + models.length + ' reranker models from API');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var html = '<div class="space-y-2">';
|
var html = '<div class="space-y-2">';
|
||||||
|
|
||||||
// Show current backend status
|
// Show current backend status
|
||||||
var backendLabel = currentBackend === 'litellm' ? 'API (LiteLLM)' : 'Local (FastEmbed)';
|
var isApiBackend = currentBackend === 'litellm' || currentBackend === 'api';
|
||||||
var backendIcon = currentBackend === 'litellm' ? 'cloud' : 'hard-drive';
|
var backendLabel = isApiBackend ? 'API (' + (currentBackend === 'litellm' ? 'LiteLLM' : 'Remote') + ')' : 'Local (FastEmbed)';
|
||||||
|
var backendIcon = isApiBackend ? 'cloud' : 'hard-drive';
|
||||||
html +=
|
html +=
|
||||||
'<div class="flex items-center justify-between p-2 bg-primary/5 rounded border border-primary/20 mb-3">' +
|
'<div class="flex items-center justify-between p-2 bg-primary/5 rounded border border-primary/20 mb-3">' +
|
||||||
'<div class="flex items-center gap-2">' +
|
'<div class="flex items-center gap-2">' +
|
||||||
@@ -1753,60 +1839,95 @@ async function loadRerankerModelList() {
|
|||||||
'<span class="text-xs text-muted-foreground">via Environment Variables</span>' +
|
'<span class="text-xs text-muted-foreground">via Environment Variables</span>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
|
|
||||||
// Show models for local backend only
|
// Helper to match model names (handles different prefixes like Xenova/ vs cross-encoder/)
|
||||||
if (currentBackend === 'fastembed' || currentBackend === 'onnx') {
|
function modelMatches(current, target) {
|
||||||
// Helper to match model names (handles different prefixes like Xenova/ vs cross-encoder/)
|
if (!current || !target) return false;
|
||||||
function modelMatches(current, target) {
|
// Exact match
|
||||||
if (!current || !target) return false;
|
if (current === target) return true;
|
||||||
// Exact match
|
// Match by base name (after last /)
|
||||||
if (current === target) return true;
|
var currentBase = current.split('/').pop();
|
||||||
// Match by base name (after last /)
|
var targetBase = target.split('/').pop();
|
||||||
var currentBase = current.split('/').pop();
|
return currentBase === targetBase;
|
||||||
var targetBase = target.split('/').pop();
|
}
|
||||||
return currentBase === targetBase;
|
|
||||||
}
|
|
||||||
|
|
||||||
RERANKER_MODELS.forEach(function(model) {
|
// Show API info when using API backend
|
||||||
var isActive = modelMatches(currentModel, model.name);
|
if (isApiBackend) {
|
||||||
var statusIcon = isActive
|
|
||||||
? '<i data-lucide="check-circle" class="w-3.5 h-3.5 text-success"></i>'
|
|
||||||
: '<i data-lucide="circle" class="w-3.5 h-3.5 text-muted"></i>';
|
|
||||||
|
|
||||||
var actionBtn = isActive
|
|
||||||
? '<span class="text-xs text-success">Active</span>'
|
|
||||||
: '<button class="text-xs text-primary hover:underline" onclick="selectRerankerModel(\'' + model.name + '\')">Select</button>';
|
|
||||||
|
|
||||||
html +=
|
|
||||||
'<div class="flex items-center justify-between p-2 bg-muted/30 rounded" id="reranker-' + model.id + '">' +
|
|
||||||
'<div class="flex items-center gap-2">' +
|
|
||||||
statusIcon +
|
|
||||||
'<span class="text-sm font-medium">' + model.id + '</span>' +
|
|
||||||
'<span class="text-xs text-muted-foreground">' + model.desc + '</span>' +
|
|
||||||
'</div>' +
|
|
||||||
'<div class="flex items-center gap-3">' +
|
|
||||||
'<span class="text-xs text-muted-foreground">~' + model.size + ' MB</span>' +
|
|
||||||
actionBtn +
|
|
||||||
'</div>' +
|
|
||||||
'</div>';
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// API/LiteLLM backend - show current model info
|
|
||||||
html +=
|
html +=
|
||||||
'<div class="p-3 bg-muted/30 rounded">' +
|
'<div class="p-3 bg-blue-500/10 rounded border border-blue-500/20 mb-3">' +
|
||||||
'<div class="flex items-center justify-center gap-2 mb-2">' +
|
'<div class="flex items-center gap-2 mb-2">' +
|
||||||
'<i data-lucide="cloud" class="w-5 h-5 text-primary"></i>' +
|
'<i data-lucide="cloud" class="w-4 h-4 text-blue-500"></i>' +
|
||||||
'<span class="text-sm font-medium">Using API Reranker</span>' +
|
'<span class="text-sm font-medium">' + t('codexlens.usingApiReranker') + '</span>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="text-center">' +
|
'<div class="flex items-center gap-2">' +
|
||||||
'<div class="text-xs text-muted-foreground mb-1">Current Model:</div>' +
|
'<span class="text-xs text-muted-foreground">' + t('codexlens.currentModel') + ':</span>' +
|
||||||
'<div class="text-sm font-mono bg-background px-2 py-1 rounded border border-border inline-block">' +
|
'<span class="text-xs font-mono bg-background px-2 py-0.5 rounded border border-border">' +
|
||||||
escapeHtml(currentModel) +
|
escapeHtml(currentModel) +
|
||||||
'</div>' +
|
'</span>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="text-xs text-muted-foreground mt-2 text-center">Configure via Environment Variables below</div>' +
|
|
||||||
'</div>';
|
'</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Local models section title
|
||||||
|
html +=
|
||||||
|
'<div class="text-xs font-medium text-muted-foreground mb-2 flex items-center gap-2">' +
|
||||||
|
'<i data-lucide="hard-drive" class="w-3.5 h-3.5"></i>' +
|
||||||
|
t('codexlens.localModels') +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
|
models.forEach(function(model) {
|
||||||
|
var isActive = !isApiBackend && modelMatches(currentModel, model.name);
|
||||||
|
var isInstalled = model.installed;
|
||||||
|
|
||||||
|
// Status icon
|
||||||
|
var statusIcon;
|
||||||
|
if (isActive) {
|
||||||
|
statusIcon = '<i data-lucide="check-circle" class="w-3.5 h-3.5 text-success"></i>';
|
||||||
|
} else if (isInstalled) {
|
||||||
|
statusIcon = '<i data-lucide="check" class="w-3.5 h-3.5 text-primary"></i>';
|
||||||
|
} else {
|
||||||
|
statusIcon = '<i data-lucide="circle" class="w-3.5 h-3.5 text-muted"></i>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action buttons
|
||||||
|
var actionBtns = '';
|
||||||
|
if (isActive) {
|
||||||
|
actionBtns = '<span class="text-xs text-success">' + t('codexlens.active') + '</span>';
|
||||||
|
if (isInstalled) {
|
||||||
|
actionBtns += '<button class="text-xs text-destructive hover:underline ml-2" onclick="deleteRerankerModel(\'' + model.id + '\')">' + t('codexlens.deleteModel') + '</button>';
|
||||||
|
}
|
||||||
|
} else if (isInstalled) {
|
||||||
|
// Installed but not active - can select or delete
|
||||||
|
if (isApiBackend) {
|
||||||
|
actionBtns = '<button class="text-xs text-primary hover:underline" onclick="switchToLocalReranker(\'' + model.name + '\')">' + t('codexlens.useLocal') + '</button>';
|
||||||
|
} else {
|
||||||
|
actionBtns = '<button class="text-xs text-primary hover:underline" onclick="selectRerankerModel(\'' + model.name + '\')">' + t('codexlens.select') + '</button>';
|
||||||
|
}
|
||||||
|
actionBtns += '<button class="text-xs text-destructive hover:underline ml-2" onclick="deleteRerankerModel(\'' + model.id + '\')">' + t('codexlens.deleteModel') + '</button>';
|
||||||
|
} else {
|
||||||
|
// Not installed - show download button
|
||||||
|
actionBtns = '<button class="text-xs text-primary hover:underline" onclick="downloadRerankerModel(\'' + model.id + '\')">' + t('codexlens.downloadModel') + '</button>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size display
|
||||||
|
var sizeText = (isInstalled && model.size) ? model.size + ' MB' : '~' + model.size + ' MB';
|
||||||
|
|
||||||
|
// Recommendation badge
|
||||||
|
var recBadge = model.recommended ? ' <span class="text-xs text-yellow-500">★</span>' : '';
|
||||||
|
|
||||||
|
html +=
|
||||||
|
'<div class="flex items-center justify-between p-2 bg-muted/30 rounded" id="reranker-' + model.id + '">' +
|
||||||
|
'<div class="flex items-center gap-2">' +
|
||||||
|
statusIcon +
|
||||||
|
'<span class="text-sm font-medium">' + model.id + recBadge + '</span>' +
|
||||||
|
'<span class="text-xs text-muted-foreground">' + model.desc + '</span>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="flex items-center gap-3">' +
|
||||||
|
'<span class="text-xs text-muted-foreground">' + sizeText + '</span>' +
|
||||||
|
actionBtns +
|
||||||
|
'</div>' +
|
||||||
|
'</div>';
|
||||||
|
});
|
||||||
|
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
// Update all containers
|
// Update all containers
|
||||||
containers.forEach(function(container) {
|
containers.forEach(function(container) {
|
||||||
@@ -1821,6 +1942,68 @@ async function loadRerankerModelList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download reranker model
|
||||||
|
*/
|
||||||
|
async function downloadRerankerModel(profile) {
|
||||||
|
var container = document.getElementById('reranker-' + profile);
|
||||||
|
if (container) {
|
||||||
|
container.innerHTML =
|
||||||
|
'<div class="flex items-center gap-2 p-2">' +
|
||||||
|
'<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i>' +
|
||||||
|
'<span class="text-sm">' + t('codexlens.downloading') + '</span>' +
|
||||||
|
'</div>';
|
||||||
|
if (window.lucide) lucide.createIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var response = await fetch('/api/codexlens/reranker/models/download', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ profile: profile })
|
||||||
|
});
|
||||||
|
var result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
showRefreshToast(t('codexlens.downloadComplete') + ': ' + profile, 'success');
|
||||||
|
loadRerankerModelList();
|
||||||
|
} else {
|
||||||
|
showRefreshToast(t('codexlens.downloadFailed') + ': ' + (result.error || 'Unknown error'), 'error');
|
||||||
|
loadRerankerModelList();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showRefreshToast(t('codexlens.downloadFailed') + ': ' + err.message, 'error');
|
||||||
|
loadRerankerModelList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete reranker model
|
||||||
|
*/
|
||||||
|
async function deleteRerankerModel(profile) {
|
||||||
|
if (!confirm(t('codexlens.deleteModelConfirm') + ' ' + profile + '?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var response = await fetch('/api/codexlens/reranker/models/delete', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ profile: profile })
|
||||||
|
});
|
||||||
|
var result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
showRefreshToast(t('codexlens.modelDeleted') + ': ' + profile, 'success');
|
||||||
|
loadRerankerModelList();
|
||||||
|
} else {
|
||||||
|
showRefreshToast('Failed to delete: ' + (result.error || 'Unknown error'), 'error');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showRefreshToast('Error: ' + err.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update reranker backend
|
* Update reranker backend
|
||||||
*/
|
*/
|
||||||
@@ -1867,6 +2050,47 @@ async function selectRerankerModel(modelName) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch from API to local reranker backend and select model
|
||||||
|
*/
|
||||||
|
async function switchToLocalReranker(modelName) {
|
||||||
|
try {
|
||||||
|
// First switch backend to fastembed
|
||||||
|
var backendResponse = await fetch('/api/codexlens/reranker/config', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ backend: 'fastembed' })
|
||||||
|
});
|
||||||
|
var backendResult = await backendResponse.json();
|
||||||
|
|
||||||
|
if (!backendResult.success) {
|
||||||
|
showRefreshToast('Failed to switch backend: ' + (backendResult.error || 'Unknown error'), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then select the model
|
||||||
|
var modelResponse = await fetch('/api/codexlens/reranker/config', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ model_name: modelName })
|
||||||
|
});
|
||||||
|
var modelResult = await modelResponse.json();
|
||||||
|
|
||||||
|
if (modelResult.success) {
|
||||||
|
showRefreshToast(t('codexlens.switchedToLocal') + ': ' + modelName.split('/').pop(), 'success');
|
||||||
|
loadRerankerModelList();
|
||||||
|
// Also reload env variables to reflect the change
|
||||||
|
if (typeof loadEnvVariables === 'function') {
|
||||||
|
loadEnvVariables();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showRefreshToast('Failed to select model: ' + (modelResult.error || 'Unknown error'), 'error');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showRefreshToast('Error: ' + err.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// MODEL TAB & MODE MANAGEMENT
|
// MODEL TAB & MODE MANAGEMENT
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -1896,12 +2120,16 @@ function switchCodexLensModelTab(tabName) {
|
|||||||
|
|
||||||
if (embeddingContent && rerankerContent) {
|
if (embeddingContent && rerankerContent) {
|
||||||
if (tabName === 'embedding') {
|
if (tabName === 'embedding') {
|
||||||
|
embeddingContent.classList.remove('hidden');
|
||||||
embeddingContent.style.display = 'block';
|
embeddingContent.style.display = 'block';
|
||||||
|
rerankerContent.classList.add('hidden');
|
||||||
rerankerContent.style.display = 'none';
|
rerankerContent.style.display = 'none';
|
||||||
// Reload embedding models when switching to embedding tab
|
// Reload embedding models when switching to embedding tab
|
||||||
loadModelList();
|
loadModelList();
|
||||||
} else {
|
} else {
|
||||||
|
embeddingContent.classList.add('hidden');
|
||||||
embeddingContent.style.display = 'none';
|
embeddingContent.style.display = 'none';
|
||||||
|
rerankerContent.classList.remove('hidden');
|
||||||
rerankerContent.style.display = 'block';
|
rerankerContent.style.display = 'block';
|
||||||
// Load reranker models when switching to reranker tab
|
// Load reranker models when switching to reranker tab
|
||||||
loadRerankerModelList();
|
loadRerankerModelList();
|
||||||
|
|||||||
@@ -1975,6 +1975,175 @@ def model_info(
|
|||||||
console.print(f" Use case: {data['use_case']}")
|
console.print(f" Use case: {data['use_case']}")
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== Reranker Model Management Commands ====================
|
||||||
|
|
||||||
|
|
||||||
|
@app.command(name="reranker-model-list")
|
||||||
|
def reranker_model_list(
|
||||||
|
json_mode: bool = typer.Option(False, "--json", help="Output JSON response."),
|
||||||
|
) -> None:
|
||||||
|
"""List available reranker models and their installation status.
|
||||||
|
|
||||||
|
Shows reranker model profiles with:
|
||||||
|
- Installation status
|
||||||
|
- Model size
|
||||||
|
- Use case recommendations
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from codexlens.cli.model_manager import list_reranker_models
|
||||||
|
|
||||||
|
result = list_reranker_models()
|
||||||
|
|
||||||
|
if json_mode:
|
||||||
|
print_json(**result)
|
||||||
|
else:
|
||||||
|
if not result["success"]:
|
||||||
|
console.print(f"[red]Error:[/red] {result.get('error', 'Unknown error')}")
|
||||||
|
raise typer.Exit(code=1)
|
||||||
|
|
||||||
|
data = result["result"]
|
||||||
|
models = data["models"]
|
||||||
|
cache_dir = data["cache_dir"]
|
||||||
|
cache_exists = data["cache_exists"]
|
||||||
|
|
||||||
|
console.print("[bold]Available Reranker Models:[/bold]")
|
||||||
|
console.print(f"Cache directory: [dim]{cache_dir}[/dim] {'(exists)' if cache_exists else '(not found)'}\n")
|
||||||
|
|
||||||
|
table = Table(show_header=True, header_style="bold")
|
||||||
|
table.add_column("Profile", style="cyan")
|
||||||
|
table.add_column("Model", style="dim")
|
||||||
|
table.add_column("Size", justify="right")
|
||||||
|
table.add_column("Status")
|
||||||
|
table.add_column("Description")
|
||||||
|
|
||||||
|
for m in models:
|
||||||
|
status = "[green]✓ Installed[/green]" if m["installed"] else "[dim]Not installed[/dim]"
|
||||||
|
size = f"{m['actual_size_mb']:.1f} MB" if m["installed"] and m["actual_size_mb"] else f"~{m['estimated_size_mb']} MB"
|
||||||
|
rec = " [yellow]★[/yellow]" if m.get("recommended") else ""
|
||||||
|
table.add_row(m["profile"] + rec, m["model_name"], size, status, m["description"])
|
||||||
|
|
||||||
|
console.print(table)
|
||||||
|
console.print("\n[yellow]★[/yellow] = Recommended")
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
if json_mode:
|
||||||
|
print_json(success=False, error="fastembed reranker not available. Install with: pip install fastembed>=0.4.0")
|
||||||
|
else:
|
||||||
|
console.print("[red]Error:[/red] fastembed reranker not available")
|
||||||
|
console.print("Install with: [cyan]pip install fastembed>=0.4.0[/cyan]")
|
||||||
|
raise typer.Exit(code=1)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command(name="reranker-model-download")
|
||||||
|
def reranker_model_download(
|
||||||
|
profile: str = typer.Argument(..., help="Reranker model profile to download."),
|
||||||
|
json_mode: bool = typer.Option(False, "--json", help="Output JSON response."),
|
||||||
|
) -> None:
|
||||||
|
"""Download a reranker model by profile name.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
codexlens reranker-model-download ms-marco-mini # Download default reranker
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from codexlens.cli.model_manager import download_reranker_model
|
||||||
|
|
||||||
|
if not json_mode:
|
||||||
|
console.print(f"[bold]Downloading reranker model:[/bold] {profile}")
|
||||||
|
console.print("[dim]This may take a few minutes depending on your internet connection...[/dim]\n")
|
||||||
|
|
||||||
|
progress_callback = None if json_mode else lambda msg: console.print(f"[cyan]{msg}[/cyan]")
|
||||||
|
|
||||||
|
result = download_reranker_model(profile, progress_callback=progress_callback)
|
||||||
|
|
||||||
|
if json_mode:
|
||||||
|
print_json(**result)
|
||||||
|
else:
|
||||||
|
if not result["success"]:
|
||||||
|
console.print(f"[red]Error:[/red] {result.get('error', 'Unknown error')}")
|
||||||
|
raise typer.Exit(code=1)
|
||||||
|
|
||||||
|
data = result["result"]
|
||||||
|
console.print(f"[green]✓[/green] Reranker model downloaded successfully!")
|
||||||
|
console.print(f" Profile: {data['profile']}")
|
||||||
|
console.print(f" Model: {data['model_name']}")
|
||||||
|
console.print(f" Cache size: {data['cache_size_mb']:.1f} MB")
|
||||||
|
console.print(f" Location: [dim]{data['cache_path']}[/dim]")
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
if json_mode:
|
||||||
|
print_json(success=False, error="fastembed reranker not available. Install with: pip install fastembed>=0.4.0")
|
||||||
|
else:
|
||||||
|
console.print("[red]Error:[/red] fastembed reranker not available")
|
||||||
|
console.print("Install with: [cyan]pip install fastembed>=0.4.0[/cyan]")
|
||||||
|
raise typer.Exit(code=1)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command(name="reranker-model-delete")
|
||||||
|
def reranker_model_delete(
|
||||||
|
profile: str = typer.Argument(..., help="Reranker model profile to delete."),
|
||||||
|
json_mode: bool = typer.Option(False, "--json", help="Output JSON response."),
|
||||||
|
) -> None:
|
||||||
|
"""Delete a downloaded reranker model from cache.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
codexlens reranker-model-delete ms-marco-mini # Delete reranker model
|
||||||
|
"""
|
||||||
|
from codexlens.cli.model_manager import delete_reranker_model
|
||||||
|
|
||||||
|
if not json_mode:
|
||||||
|
console.print(f"[bold yellow]Deleting reranker model:[/bold yellow] {profile}")
|
||||||
|
|
||||||
|
result = delete_reranker_model(profile)
|
||||||
|
|
||||||
|
if json_mode:
|
||||||
|
print_json(**result)
|
||||||
|
else:
|
||||||
|
if not result["success"]:
|
||||||
|
console.print(f"[red]Error:[/red] {result.get('error', 'Unknown error')}")
|
||||||
|
raise typer.Exit(code=1)
|
||||||
|
|
||||||
|
data = result["result"]
|
||||||
|
console.print(f"[green]✓[/green] Reranker model deleted successfully!")
|
||||||
|
console.print(f" Profile: {data['profile']}")
|
||||||
|
console.print(f" Model: {data['model_name']}")
|
||||||
|
console.print(f" Freed space: {data['deleted_size_mb']:.1f} MB")
|
||||||
|
|
||||||
|
|
||||||
|
@app.command(name="reranker-model-info")
|
||||||
|
def reranker_model_info(
|
||||||
|
profile: str = typer.Argument(..., help="Reranker model profile to get info."),
|
||||||
|
json_mode: bool = typer.Option(False, "--json", help="Output JSON response."),
|
||||||
|
) -> None:
|
||||||
|
"""Get detailed information about a reranker model profile.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
codexlens reranker-model-info ms-marco-mini # Get reranker model details
|
||||||
|
"""
|
||||||
|
from codexlens.cli.model_manager import get_reranker_model_info
|
||||||
|
|
||||||
|
result = get_reranker_model_info(profile)
|
||||||
|
|
||||||
|
if json_mode:
|
||||||
|
print_json(**result)
|
||||||
|
else:
|
||||||
|
if not result["success"]:
|
||||||
|
console.print(f"[red]Error:[/red] {result.get('error', 'Unknown error')}")
|
||||||
|
raise typer.Exit(code=1)
|
||||||
|
|
||||||
|
data = result["result"]
|
||||||
|
console.print(f"[bold]Reranker Model Profile:[/bold] {data['profile']}")
|
||||||
|
console.print(f" Model name: {data['model_name']}")
|
||||||
|
console.print(f" Status: {'[green]Installed[/green]' if data['installed'] else '[dim]Not installed[/dim]'}")
|
||||||
|
if data['installed'] and data['actual_size_mb']:
|
||||||
|
console.print(f" Cache size: {data['actual_size_mb']:.1f} MB")
|
||||||
|
console.print(f" Location: [dim]{data['cache_path']}[/dim]")
|
||||||
|
else:
|
||||||
|
console.print(f" Estimated size: ~{data['estimated_size_mb']} MB")
|
||||||
|
console.print(f" Recommended: {'[green]Yes[/green]' if data.get('recommended') else '[dim]No[/dim]'}")
|
||||||
|
console.print(f"\n Description: {data['description']}")
|
||||||
|
console.print(f" Use case: {data['use_case']}")
|
||||||
|
|
||||||
|
|
||||||
# ==================== Embedding Management Commands ====================
|
# ==================== Embedding Management Commands ====================
|
||||||
|
|
||||||
@app.command(name="embeddings-status", hidden=True, deprecated=True)
|
@app.command(name="embeddings-status", hidden=True, deprecated=True)
|
||||||
|
|||||||
@@ -12,6 +12,66 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
FASTEMBED_AVAILABLE = False
|
FASTEMBED_AVAILABLE = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
from fastembed import TextCrossEncoder
|
||||||
|
RERANKER_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
RERANKER_AVAILABLE = False
|
||||||
|
|
||||||
|
|
||||||
|
# Reranker model profiles with metadata
|
||||||
|
# Note: fastembed TextCrossEncoder uses ONNX models from HuggingFace
|
||||||
|
RERANKER_MODEL_PROFILES = {
|
||||||
|
"ms-marco-mini": {
|
||||||
|
"model_name": "Xenova/ms-marco-MiniLM-L-6-v2",
|
||||||
|
"cache_name": "Xenova/ms-marco-MiniLM-L-6-v2",
|
||||||
|
"size_mb": 90,
|
||||||
|
"description": "Fast, lightweight reranker (default)",
|
||||||
|
"use_case": "Quick prototyping, resource-constrained environments",
|
||||||
|
"recommended": True,
|
||||||
|
},
|
||||||
|
"ms-marco-12": {
|
||||||
|
"model_name": "Xenova/ms-marco-MiniLM-L-12-v2",
|
||||||
|
"cache_name": "Xenova/ms-marco-MiniLM-L-12-v2",
|
||||||
|
"size_mb": 130,
|
||||||
|
"description": "Better quality, 12-layer MiniLM",
|
||||||
|
"use_case": "General purpose reranking with better accuracy",
|
||||||
|
"recommended": True,
|
||||||
|
},
|
||||||
|
"bge-base": {
|
||||||
|
"model_name": "BAAI/bge-reranker-base",
|
||||||
|
"cache_name": "BAAI/bge-reranker-base",
|
||||||
|
"size_mb": 280,
|
||||||
|
"description": "BGE reranker base model",
|
||||||
|
"use_case": "High-quality reranking for production",
|
||||||
|
"recommended": True,
|
||||||
|
},
|
||||||
|
"bge-large": {
|
||||||
|
"model_name": "BAAI/bge-reranker-large",
|
||||||
|
"cache_name": "BAAI/bge-reranker-large",
|
||||||
|
"size_mb": 560,
|
||||||
|
"description": "BGE reranker large model (high resource usage)",
|
||||||
|
"use_case": "Maximum quality reranking",
|
||||||
|
"recommended": False,
|
||||||
|
},
|
||||||
|
"jina-tiny": {
|
||||||
|
"model_name": "jinaai/jina-reranker-v1-tiny-en",
|
||||||
|
"cache_name": "jinaai/jina-reranker-v1-tiny-en",
|
||||||
|
"size_mb": 70,
|
||||||
|
"description": "Jina tiny reranker, very fast",
|
||||||
|
"use_case": "Ultra-low latency applications",
|
||||||
|
"recommended": True,
|
||||||
|
},
|
||||||
|
"jina-turbo": {
|
||||||
|
"model_name": "jinaai/jina-reranker-v1-turbo-en",
|
||||||
|
"cache_name": "jinaai/jina-reranker-v1-turbo-en",
|
||||||
|
"size_mb": 150,
|
||||||
|
"description": "Jina turbo reranker, balanced",
|
||||||
|
"use_case": "Fast reranking with good accuracy",
|
||||||
|
"recommended": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Model profiles with metadata
|
# Model profiles with metadata
|
||||||
# Note: 768d is max recommended dimension for optimal performance/quality balance
|
# Note: 768d is max recommended dimension for optimal performance/quality balance
|
||||||
@@ -348,3 +408,235 @@ def get_model_info(profile: str) -> Dict[str, any]:
|
|||||||
"cache_path": str(model_cache_path) if installed else None,
|
"cache_path": str(model_cache_path) if installed else None,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Reranker Model Management Functions
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def list_reranker_models() -> Dict[str, any]:
|
||||||
|
"""List available reranker model profiles and their installation status.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with reranker model profiles, installed status, and cache info
|
||||||
|
"""
|
||||||
|
if not RERANKER_AVAILABLE:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "fastembed reranker not available. Install with: pip install fastembed>=0.4.0",
|
||||||
|
}
|
||||||
|
|
||||||
|
cache_dir = get_cache_dir()
|
||||||
|
cache_exists = cache_dir.exists()
|
||||||
|
|
||||||
|
models = []
|
||||||
|
for profile, info in RERANKER_MODEL_PROFILES.items():
|
||||||
|
model_name = info["model_name"]
|
||||||
|
|
||||||
|
# Check if model is cached
|
||||||
|
installed = False
|
||||||
|
cache_size_mb = 0
|
||||||
|
|
||||||
|
if cache_exists:
|
||||||
|
model_cache_path = _get_model_cache_path(cache_dir, info)
|
||||||
|
if model_cache_path.exists():
|
||||||
|
installed = True
|
||||||
|
total_size = sum(
|
||||||
|
f.stat().st_size
|
||||||
|
for f in model_cache_path.rglob("*")
|
||||||
|
if f.is_file()
|
||||||
|
)
|
||||||
|
cache_size_mb = round(total_size / (1024 * 1024), 1)
|
||||||
|
|
||||||
|
models.append({
|
||||||
|
"profile": profile,
|
||||||
|
"model_name": model_name,
|
||||||
|
"estimated_size_mb": info["size_mb"],
|
||||||
|
"actual_size_mb": cache_size_mb if installed else None,
|
||||||
|
"description": info["description"],
|
||||||
|
"use_case": info["use_case"],
|
||||||
|
"installed": installed,
|
||||||
|
"recommended": info.get("recommended", True),
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"result": {
|
||||||
|
"models": models,
|
||||||
|
"cache_dir": str(cache_dir),
|
||||||
|
"cache_exists": cache_exists,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def download_reranker_model(profile: str, progress_callback: Optional[callable] = None) -> Dict[str, any]:
|
||||||
|
"""Download a reranker model by profile name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
profile: Reranker model profile name
|
||||||
|
progress_callback: Optional callback function to report progress
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Result dictionary with success status
|
||||||
|
"""
|
||||||
|
if not RERANKER_AVAILABLE:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "fastembed reranker not available. Install with: pip install fastembed>=0.4.0",
|
||||||
|
}
|
||||||
|
|
||||||
|
if profile not in RERANKER_MODEL_PROFILES:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Unknown reranker profile: {profile}. Available: {', '.join(RERANKER_MODEL_PROFILES.keys())}",
|
||||||
|
}
|
||||||
|
|
||||||
|
info = RERANKER_MODEL_PROFILES[profile]
|
||||||
|
model_name = info["model_name"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
cache_dir = get_cache_dir()
|
||||||
|
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(f"Downloading reranker {model_name}...")
|
||||||
|
|
||||||
|
# Download model by instantiating TextCrossEncoder with explicit cache_dir
|
||||||
|
reranker = TextCrossEncoder(model_name=model_name, cache_dir=str(cache_dir))
|
||||||
|
|
||||||
|
# Trigger actual download by calling rerank
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(f"Initializing {model_name}...")
|
||||||
|
|
||||||
|
list(reranker.rerank("test query", ["test document"]))
|
||||||
|
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(f"Reranker {model_name} downloaded successfully")
|
||||||
|
|
||||||
|
# Get cache info
|
||||||
|
model_cache_path = _get_model_cache_path(cache_dir, info)
|
||||||
|
|
||||||
|
cache_size = 0
|
||||||
|
if model_cache_path.exists():
|
||||||
|
total_size = sum(
|
||||||
|
f.stat().st_size
|
||||||
|
for f in model_cache_path.rglob("*")
|
||||||
|
if f.is_file()
|
||||||
|
)
|
||||||
|
cache_size = round(total_size / (1024 * 1024), 1)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"result": {
|
||||||
|
"profile": profile,
|
||||||
|
"model_name": model_name,
|
||||||
|
"cache_size_mb": cache_size,
|
||||||
|
"cache_path": str(model_cache_path),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Failed to download reranker model: {str(e)}",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def delete_reranker_model(profile: str) -> Dict[str, any]:
|
||||||
|
"""Delete a downloaded reranker model from cache.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
profile: Reranker model profile name to delete
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Result dictionary with success status
|
||||||
|
"""
|
||||||
|
if profile not in RERANKER_MODEL_PROFILES:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Unknown reranker profile: {profile}. Available: {', '.join(RERANKER_MODEL_PROFILES.keys())}",
|
||||||
|
}
|
||||||
|
|
||||||
|
info = RERANKER_MODEL_PROFILES[profile]
|
||||||
|
model_name = info["model_name"]
|
||||||
|
cache_dir = get_cache_dir()
|
||||||
|
model_cache_path = _get_model_cache_path(cache_dir, info)
|
||||||
|
|
||||||
|
if not model_cache_path.exists():
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Reranker model {profile} ({model_name}) is not installed",
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
total_size = sum(
|
||||||
|
f.stat().st_size
|
||||||
|
for f in model_cache_path.rglob("*")
|
||||||
|
if f.is_file()
|
||||||
|
)
|
||||||
|
size_mb = round(total_size / (1024 * 1024), 1)
|
||||||
|
|
||||||
|
shutil.rmtree(model_cache_path)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"result": {
|
||||||
|
"profile": profile,
|
||||||
|
"model_name": model_name,
|
||||||
|
"deleted_size_mb": size_mb,
|
||||||
|
"cache_path": str(model_cache_path),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Failed to delete reranker model: {str(e)}",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_reranker_model_info(profile: str) -> Dict[str, any]:
|
||||||
|
"""Get detailed information about a reranker model profile.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
profile: Reranker model profile name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Result dictionary with model information
|
||||||
|
"""
|
||||||
|
if profile not in RERANKER_MODEL_PROFILES:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Unknown reranker profile: {profile}. Available: {', '.join(RERANKER_MODEL_PROFILES.keys())}",
|
||||||
|
}
|
||||||
|
|
||||||
|
info = RERANKER_MODEL_PROFILES[profile]
|
||||||
|
model_name = info["model_name"]
|
||||||
|
|
||||||
|
cache_dir = get_cache_dir()
|
||||||
|
model_cache_path = _get_model_cache_path(cache_dir, info)
|
||||||
|
installed = model_cache_path.exists()
|
||||||
|
|
||||||
|
cache_size_mb = None
|
||||||
|
if installed:
|
||||||
|
total_size = sum(
|
||||||
|
f.stat().st_size
|
||||||
|
for f in model_cache_path.rglob("*")
|
||||||
|
if f.is_file()
|
||||||
|
)
|
||||||
|
cache_size_mb = round(total_size / (1024 * 1024), 1)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"result": {
|
||||||
|
"profile": profile,
|
||||||
|
"model_name": model_name,
|
||||||
|
"estimated_size_mb": info["size_mb"],
|
||||||
|
"actual_size_mb": cache_size_mb,
|
||||||
|
"description": info["description"],
|
||||||
|
"use_case": info["use_case"],
|
||||||
|
"installed": installed,
|
||||||
|
"recommended": info.get("recommended", True),
|
||||||
|
"cache_path": str(model_cache_path) if installed else None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user