mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-12 02:37:45 +08:00
feat: Add custom model download functionality and enhance model management
- Implemented `model-download-custom` command to download HuggingFace models. - Added support for discovering manually placed models in the cache. - Enhanced the model list view to display recommended and discovered models separately. - Introduced JSON editor for direct configuration mode in API settings. - Added validation and formatting features for JSON input. - Updated translations for new API settings and common actions. - Improved user interface for model management, including action buttons and tooltips.
This commit is contained in:
@@ -497,6 +497,46 @@ export async function handleCodexLensConfigRoutes(ctx: RouteContext): Promise<bo
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API: CodexLens Model Download Custom (download any HuggingFace model)
|
||||||
|
if (pathname === '/api/codexlens/models/download-custom' && req.method === 'POST') {
|
||||||
|
handlePostRequest(req, res, async (body) => {
|
||||||
|
const { model_name, model_type } = body as { model_name?: unknown; model_type?: unknown };
|
||||||
|
const resolvedModelName = typeof model_name === 'string' && model_name.trim().length > 0 ? model_name.trim() : undefined;
|
||||||
|
const resolvedModelType = typeof model_type === 'string' ? model_type.trim() : 'embedding';
|
||||||
|
|
||||||
|
if (!resolvedModelName) {
|
||||||
|
return { success: false, error: 'model_name is required', status: 400 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate model name format
|
||||||
|
if (!resolvedModelName.includes('/')) {
|
||||||
|
return { success: false, error: 'Invalid model_name format. Expected: org/model-name', status: 400 };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await executeCodexLens([
|
||||||
|
'model-download-custom', resolvedModelName,
|
||||||
|
'--type', resolvedModelType,
|
||||||
|
'--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: unknown) {
|
||||||
|
return { success: false, error: err instanceof Error ? err.message : String(err), status: 500 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// API: CodexLens Model Delete (delete embedding model by profile)
|
// API: CodexLens Model Delete (delete embedding model by profile)
|
||||||
if (pathname === '/api/codexlens/models/delete' && req.method === 'POST') {
|
if (pathname === '/api/codexlens/models/delete' && req.method === 'POST') {
|
||||||
handlePostRequest(req, res, async (body) => {
|
handlePostRequest(req, res, async (body) => {
|
||||||
@@ -526,6 +566,47 @@ export async function handleCodexLensConfigRoutes(ctx: RouteContext): Promise<bo
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API: CodexLens Model Delete by Path (delete discovered/manually placed model)
|
||||||
|
if (pathname === '/api/codexlens/models/delete-path' && req.method === 'POST') {
|
||||||
|
handlePostRequest(req, res, async (body) => {
|
||||||
|
const { cache_path } = body as { cache_path?: unknown };
|
||||||
|
const resolvedPath = typeof cache_path === 'string' && cache_path.trim().length > 0 ? cache_path.trim() : undefined;
|
||||||
|
|
||||||
|
if (!resolvedPath) {
|
||||||
|
return { success: false, error: 'cache_path is required', status: 400 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security: Validate that the path is within the HuggingFace cache directory
|
||||||
|
const { homedir } = await import('os');
|
||||||
|
const { join, resolve, normalize } = await import('path');
|
||||||
|
const { rm } = await import('fs/promises');
|
||||||
|
|
||||||
|
const hfCacheDir = process.env.HF_HOME || join(homedir(), '.cache', 'huggingface');
|
||||||
|
const normalizedCachePath = normalize(resolve(resolvedPath));
|
||||||
|
const normalizedHfCacheDir = normalize(resolve(hfCacheDir));
|
||||||
|
|
||||||
|
// Ensure the path is within the HuggingFace cache directory
|
||||||
|
if (!normalizedCachePath.startsWith(normalizedHfCacheDir)) {
|
||||||
|
return { success: false, error: 'Path must be within the HuggingFace cache directory', status: 400 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure it's a models-- directory
|
||||||
|
const pathParts = normalizedCachePath.split(/[/\\]/);
|
||||||
|
const lastPart = pathParts[pathParts.length - 1];
|
||||||
|
if (!lastPart.startsWith('models--')) {
|
||||||
|
return { success: false, error: 'Path must be a model cache directory (models--*)', status: 400 };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await rm(normalizedCachePath, { recursive: true, force: true });
|
||||||
|
return { success: true, message: 'Model deleted successfully', cache_path: normalizedCachePath };
|
||||||
|
} catch (err: unknown) {
|
||||||
|
return { success: false, error: err instanceof Error ? err.message : String(err), status: 500 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// API: CodexLens Model Info (get model info by profile)
|
// API: CodexLens Model Info (get model info by profile)
|
||||||
if (pathname === '/api/codexlens/models/info' && req.method === 'GET') {
|
if (pathname === '/api/codexlens/models/info' && req.method === 'GET') {
|
||||||
const profile = url.searchParams.get('profile');
|
const profile = url.searchParams.get('profile');
|
||||||
|
|||||||
@@ -2457,4 +2457,315 @@ select.cli-input {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================
|
||||||
|
CLI Settings Form
|
||||||
|
=========================== */
|
||||||
|
|
||||||
|
/* Tool Detail Header */
|
||||||
|
.tool-detail-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-bottom: 1px solid hsl(var(--border));
|
||||||
|
background: hsl(var(--muted) / 0.2);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-detail-header h3 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-detail-header h3 svg,
|
||||||
|
.tool-detail-header h3 i {
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
color: hsl(var(--primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Claude Config Form Container */
|
||||||
|
.claude-config-form {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Config Mode Toggle */
|
||||||
|
.config-mode-toggle {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
padding: 0.25rem;
|
||||||
|
background: hsl(var(--muted) / 0.3);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-mode-btn {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.625rem 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-mode-btn:hover {
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
background: hsl(var(--muted) / 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-mode-btn.active {
|
||||||
|
color: hsl(var(--primary));
|
||||||
|
background: hsl(var(--card));
|
||||||
|
border-color: hsl(var(--border));
|
||||||
|
box-shadow: 0 1px 3px hsl(var(--foreground) / 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-mode-btn svg,
|
||||||
|
.config-mode-btn i {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Model Config Section */
|
||||||
|
.model-config-section {
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: hsl(var(--muted) / 0.2);
|
||||||
|
border: 1px solid hsl(var(--border));
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-config-section h4 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-config-section h4 svg,
|
||||||
|
.model-config-section h4 i {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
color: hsl(var(--primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-config-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-config-grid .form-group {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-config-grid .form-group label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-family: 'SF Mono', 'Consolas', 'Liberation Mono', monospace;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Actions - Sticky at bottom */
|
||||||
|
.claude-config-form .form-actions {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
margin-top: auto;
|
||||||
|
padding: 1rem 0 0;
|
||||||
|
background: hsl(var(--card));
|
||||||
|
border-top: 1px solid hsl(var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.model-config-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-mode-toggle {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================
|
||||||
|
JSON Editor Section
|
||||||
|
=========================== */
|
||||||
|
|
||||||
|
.json-editor-section {
|
||||||
|
margin-top: 1rem;
|
||||||
|
border: 1px solid hsl(var(--border));
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
background: hsl(var(--card));
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-editor-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background: hsl(var(--muted) / 0.3);
|
||||||
|
border-bottom: 1px solid hsl(var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-editor-header h4 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-editor-header h4 svg,
|
||||||
|
.json-editor-header h4 i {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
color: hsl(var(--primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-editor-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-editor-actions .btn-ghost {
|
||||||
|
padding: 0.375rem 0.625rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-editor-actions .btn-ghost:hover {
|
||||||
|
background: hsl(var(--muted) / 0.5);
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-editor-actions .btn-ghost svg,
|
||||||
|
.json-editor-actions .btn-ghost i {
|
||||||
|
width: 0.875rem;
|
||||||
|
height: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-editor-body {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
min-height: 200px;
|
||||||
|
max-height: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-line-numbers {
|
||||||
|
width: 2.5rem;
|
||||||
|
padding: 0.75rem 0.5rem;
|
||||||
|
background: hsl(var(--muted) / 0.2);
|
||||||
|
border-right: 1px solid hsl(var(--border));
|
||||||
|
font-family: 'SF Mono', 'Consolas', 'Liberation Mono', monospace;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
text-align: right;
|
||||||
|
user-select: none;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-line-numbers span {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-textarea {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: none;
|
||||||
|
resize: none;
|
||||||
|
font-family: 'SF Mono', 'Consolas', 'Liberation Mono', monospace;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
background: hsl(var(--background));
|
||||||
|
outline: none;
|
||||||
|
overflow-y: auto;
|
||||||
|
tab-size: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-textarea::placeholder {
|
||||||
|
color: hsl(var(--muted-foreground) / 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-textarea.invalid {
|
||||||
|
background: hsl(var(--destructive) / 0.05);
|
||||||
|
border-left: 2px solid hsl(var(--destructive));
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-editor-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: hsl(var(--muted) / 0.2);
|
||||||
|
border-top: 1px solid hsl(var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-status {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-status svg,
|
||||||
|
.json-status i {
|
||||||
|
width: 0.875rem;
|
||||||
|
height: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-status.valid {
|
||||||
|
color: hsl(142 76% 36%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-status.invalid {
|
||||||
|
color: hsl(var(--destructive));
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-hint {
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button styles for JSON editor */
|
||||||
|
.btn-sm {
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
}
|
}
|
||||||
@@ -28,6 +28,7 @@ const i18n = {
|
|||||||
'common.deleteFailed': 'Delete failed',
|
'common.deleteFailed': 'Delete failed',
|
||||||
'common.retry': 'Retry',
|
'common.retry': 'Retry',
|
||||||
'common.refresh': 'Refresh',
|
'common.refresh': 'Refresh',
|
||||||
|
'common.format': 'Format',
|
||||||
'common.back': 'Back',
|
'common.back': 'Back',
|
||||||
'common.search': 'Search...',
|
'common.search': 'Search...',
|
||||||
'common.minutes': 'minutes',
|
'common.minutes': 'minutes',
|
||||||
@@ -1771,6 +1772,14 @@ const i18n = {
|
|||||||
'apiSettings.settingsFilePath': 'Settings File Path',
|
'apiSettings.settingsFilePath': 'Settings File Path',
|
||||||
'apiSettings.nameRequired': 'Name is required',
|
'apiSettings.nameRequired': 'Name is required',
|
||||||
'apiSettings.status': 'Status',
|
'apiSettings.status': 'Status',
|
||||||
|
'apiSettings.providerBinding': 'Provider Binding',
|
||||||
|
'apiSettings.directConfig': 'Direct Configuration',
|
||||||
|
'apiSettings.modelConfig': 'Model Configuration',
|
||||||
|
'apiSettings.configJson': 'Configuration JSON',
|
||||||
|
'apiSettings.syncToJson': 'Sync to JSON',
|
||||||
|
'apiSettings.jsonEditorHint': 'Edit JSON directly to add advanced settings',
|
||||||
|
'apiSettings.jsonValid': 'Valid JSON',
|
||||||
|
'apiSettings.jsonInvalid': 'Invalid JSON',
|
||||||
|
|
||||||
// Model Pools (High Availability)
|
// Model Pools (High Availability)
|
||||||
'apiSettings.modelPools': 'Model Pools',
|
'apiSettings.modelPools': 'Model Pools',
|
||||||
@@ -2153,6 +2162,7 @@ const i18n = {
|
|||||||
'common.deleteFailed': '删除失败',
|
'common.deleteFailed': '删除失败',
|
||||||
'common.retry': '重试',
|
'common.retry': '重试',
|
||||||
'common.refresh': '刷新',
|
'common.refresh': '刷新',
|
||||||
|
'common.format': '格式化',
|
||||||
'common.back': '返回',
|
'common.back': '返回',
|
||||||
'common.search': '搜索...',
|
'common.search': '搜索...',
|
||||||
'common.minutes': '分钟',
|
'common.minutes': '分钟',
|
||||||
@@ -3906,6 +3916,14 @@ const i18n = {
|
|||||||
'apiSettings.nameRequired': '名称为必填项',
|
'apiSettings.nameRequired': '名称为必填项',
|
||||||
'apiSettings.tokenRequired': 'API 令牌为必填项',
|
'apiSettings.tokenRequired': 'API 令牌为必填项',
|
||||||
'apiSettings.status': '状态',
|
'apiSettings.status': '状态',
|
||||||
|
'apiSettings.providerBinding': '供应商绑定',
|
||||||
|
'apiSettings.directConfig': '直接配置',
|
||||||
|
'apiSettings.modelConfig': '模型配置',
|
||||||
|
'apiSettings.configJson': '配置 JSON',
|
||||||
|
'apiSettings.syncToJson': '同步到 JSON',
|
||||||
|
'apiSettings.jsonEditorHint': '直接编辑 JSON 添加高级配置',
|
||||||
|
'apiSettings.jsonValid': 'JSON 有效',
|
||||||
|
'apiSettings.jsonInvalid': 'JSON 无效',
|
||||||
|
|
||||||
// Model Pools (High Availability)
|
// Model Pools (High Availability)
|
||||||
'apiSettings.modelPools': '高可用',
|
'apiSettings.modelPools': '高可用',
|
||||||
|
|||||||
@@ -4031,10 +4031,13 @@ function renderCliConfigModeContent(existingEndpoint) {
|
|||||||
if (cliConfigMode === 'provider') {
|
if (cliConfigMode === 'provider') {
|
||||||
renderProviderModeContent(container, settings);
|
renderProviderModeContent(container, settings);
|
||||||
} else {
|
} else {
|
||||||
renderDirectModeContent(container, env);
|
renderDirectModeContent(container, env, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.lucide) lucide.createIcons();
|
if (window.lucide) lucide.createIcons();
|
||||||
|
|
||||||
|
// Initialize JSON editor after rendering
|
||||||
|
initCliJsonEditor(settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -4081,13 +4084,16 @@ function renderProviderModeContent(container, settings) {
|
|||||||
'<input type="text" id="cli-model-opus" class="form-control" placeholder="claude-3-opus-20240229" value="' + escapeHtml(env.ANTHROPIC_DEFAULT_OPUS_MODEL || '') + '" />' +
|
'<input type="text" id="cli-model-opus" class="form-control" placeholder="claude-3-opus-20240229" value="' + escapeHtml(env.ANTHROPIC_DEFAULT_OPUS_MODEL || '') + '" />' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'</div>';
|
'</div>' +
|
||||||
|
// JSON Preview/Editor Section
|
||||||
|
buildJsonEditorSection(settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render Direct Configuration mode content
|
* Render Direct Configuration mode content
|
||||||
*/
|
*/
|
||||||
function renderDirectModeContent(container, env) {
|
function renderDirectModeContent(container, env, settings) {
|
||||||
|
settings = settings || { env: env };
|
||||||
container.innerHTML =
|
container.innerHTML =
|
||||||
'<div class="form-group">' +
|
'<div class="form-group">' +
|
||||||
'<label for="cli-auth-token">ANTHROPIC_AUTH_TOKEN *</label>' +
|
'<label for="cli-auth-token">ANTHROPIC_AUTH_TOKEN *</label>' +
|
||||||
@@ -4118,9 +4124,227 @@ function renderDirectModeContent(container, env) {
|
|||||||
'<input type="text" id="cli-model-opus" class="form-control" placeholder="claude-3-opus-20240229" value="' + escapeHtml(env.ANTHROPIC_DEFAULT_OPUS_MODEL || '') + '" />' +
|
'<input type="text" id="cli-model-opus" class="form-control" placeholder="claude-3-opus-20240229" value="' + escapeHtml(env.ANTHROPIC_DEFAULT_OPUS_MODEL || '') + '" />' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
// JSON Preview/Editor Section
|
||||||
|
buildJsonEditorSection(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build JSON Editor Section HTML
|
||||||
|
*/
|
||||||
|
function buildJsonEditorSection(settings) {
|
||||||
|
return '<div class="json-editor-section">' +
|
||||||
|
'<div class="json-editor-header">' +
|
||||||
|
'<h4><i data-lucide="code-2"></i> ' + (t('apiSettings.configJson') || 'Configuration JSON') + '</h4>' +
|
||||||
|
'<div class="json-editor-actions">' +
|
||||||
|
'<button type="button" class="btn btn-sm btn-ghost" onclick="formatCliJson()" title="' + (t('common.format') || 'Format') + '">' +
|
||||||
|
'<i data-lucide="align-left"></i> ' + (t('common.format') || 'Format') +
|
||||||
|
'</button>' +
|
||||||
|
'<button type="button" class="btn btn-sm btn-ghost" onclick="syncFormToJson()" title="' + (t('apiSettings.syncToJson') || 'Sync to JSON') + '">' +
|
||||||
|
'<i data-lucide="refresh-cw"></i>' +
|
||||||
|
'</button>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="json-editor-body">' +
|
||||||
|
'<div class="json-line-numbers" id="cli-json-line-numbers"></div>' +
|
||||||
|
'<textarea id="cli-json-editor" class="json-textarea" spellcheck="false" placeholder="{\n "env": {},\n "model": ""\n}"></textarea>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="json-editor-footer">' +
|
||||||
|
'<span class="json-status" id="cli-json-status"></span>' +
|
||||||
|
'<span class="json-hint">' + (t('apiSettings.jsonEditorHint') || 'Edit JSON directly to add advanced settings') + '</span>' +
|
||||||
|
'</div>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize JSON Editor with settings data
|
||||||
|
*/
|
||||||
|
function initCliJsonEditor(settings) {
|
||||||
|
var editor = document.getElementById('cli-json-editor');
|
||||||
|
if (!editor) return;
|
||||||
|
|
||||||
|
// Build initial JSON from settings (without sensitive fields for display)
|
||||||
|
var displaySettings = buildDisplaySettings(settings);
|
||||||
|
var jsonStr = JSON.stringify(displaySettings, null, 2);
|
||||||
|
|
||||||
|
editor.value = jsonStr;
|
||||||
|
updateJsonLineNumbers();
|
||||||
|
validateCliJson();
|
||||||
|
|
||||||
|
// Add event listeners
|
||||||
|
editor.addEventListener('input', function() {
|
||||||
|
updateJsonLineNumbers();
|
||||||
|
validateCliJson();
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.addEventListener('scroll', function() {
|
||||||
|
var lineNumbers = document.getElementById('cli-json-line-numbers');
|
||||||
|
if (lineNumbers) {
|
||||||
|
lineNumbers.scrollTop = editor.scrollTop;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.addEventListener('keydown', function(e) {
|
||||||
|
// Handle Tab key for indentation
|
||||||
|
if (e.key === 'Tab') {
|
||||||
|
e.preventDefault();
|
||||||
|
var start = this.selectionStart;
|
||||||
|
var end = this.selectionEnd;
|
||||||
|
this.value = this.value.substring(0, start) + ' ' + this.value.substring(end);
|
||||||
|
this.selectionStart = this.selectionEnd = start + 2;
|
||||||
|
updateJsonLineNumbers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build display settings object (hide sensitive values)
|
||||||
|
*/
|
||||||
|
function buildDisplaySettings(settings) {
|
||||||
|
var result = {};
|
||||||
|
|
||||||
|
// Copy non-env fields
|
||||||
|
for (var key in settings) {
|
||||||
|
if (key !== 'env' && key !== 'configMode' && key !== 'providerId') {
|
||||||
|
result[key] = settings[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy env with masked sensitive values
|
||||||
|
if (settings.env) {
|
||||||
|
result.env = {};
|
||||||
|
for (var envKey in settings.env) {
|
||||||
|
if (envKey === 'ANTHROPIC_AUTH_TOKEN') {
|
||||||
|
// Mask the token
|
||||||
|
var token = settings.env[envKey] || '';
|
||||||
|
result.env[envKey] = token ? (token.substring(0, 10) + '...') : '';
|
||||||
|
} else {
|
||||||
|
result.env[envKey] = settings.env[envKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update JSON line numbers
|
||||||
|
*/
|
||||||
|
function updateJsonLineNumbers() {
|
||||||
|
var editor = document.getElementById('cli-json-editor');
|
||||||
|
var lineNumbers = document.getElementById('cli-json-line-numbers');
|
||||||
|
if (!editor || !lineNumbers) return;
|
||||||
|
|
||||||
|
var lines = editor.value.split('\n').length;
|
||||||
|
var html = '';
|
||||||
|
for (var i = 1; i <= lines; i++) {
|
||||||
|
html += '<span>' + i + '</span>';
|
||||||
|
}
|
||||||
|
lineNumbers.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate JSON in editor
|
||||||
|
*/
|
||||||
|
function validateCliJson() {
|
||||||
|
var editor = document.getElementById('cli-json-editor');
|
||||||
|
var status = document.getElementById('cli-json-status');
|
||||||
|
if (!editor || !status) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
JSON.parse(editor.value);
|
||||||
|
status.innerHTML = '<i data-lucide="check-circle"></i> ' + (t('apiSettings.jsonValid') || 'Valid JSON');
|
||||||
|
status.className = 'json-status valid';
|
||||||
|
editor.classList.remove('invalid');
|
||||||
|
if (window.lucide) lucide.createIcons();
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
status.innerHTML = '<i data-lucide="alert-circle"></i> ' + (t('apiSettings.jsonInvalid') || 'Invalid JSON') + ': ' + e.message;
|
||||||
|
status.className = 'json-status invalid';
|
||||||
|
editor.classList.add('invalid');
|
||||||
|
if (window.lucide) lucide.createIcons();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format JSON in editor
|
||||||
|
*/
|
||||||
|
function formatCliJson() {
|
||||||
|
var editor = document.getElementById('cli-json-editor');
|
||||||
|
if (!editor) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
var obj = JSON.parse(editor.value);
|
||||||
|
editor.value = JSON.stringify(obj, null, 2);
|
||||||
|
updateJsonLineNumbers();
|
||||||
|
validateCliJson();
|
||||||
|
} catch (e) {
|
||||||
|
showRefreshToast(t('apiSettings.jsonInvalid') || 'Invalid JSON', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.formatCliJson = formatCliJson;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync form fields to JSON editor
|
||||||
|
*/
|
||||||
|
function syncFormToJson() {
|
||||||
|
var editor = document.getElementById('cli-json-editor');
|
||||||
|
if (!editor) return;
|
||||||
|
|
||||||
|
// Get current JSON
|
||||||
|
var currentObj = {};
|
||||||
|
try {
|
||||||
|
currentObj = JSON.parse(editor.value);
|
||||||
|
} catch (e) {
|
||||||
|
currentObj = { env: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update env from form fields
|
||||||
|
currentObj.env = currentObj.env || {};
|
||||||
|
|
||||||
|
// Model fields
|
||||||
|
var modelDefault = document.getElementById('cli-model-default');
|
||||||
|
var modelHaiku = document.getElementById('cli-model-haiku');
|
||||||
|
var modelSonnet = document.getElementById('cli-model-sonnet');
|
||||||
|
var modelOpus = document.getElementById('cli-model-opus');
|
||||||
|
|
||||||
|
if (modelDefault && modelDefault.value) currentObj.env.ANTHROPIC_MODEL = modelDefault.value;
|
||||||
|
if (modelHaiku && modelHaiku.value) currentObj.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = modelHaiku.value;
|
||||||
|
if (modelSonnet && modelSonnet.value) currentObj.env.ANTHROPIC_DEFAULT_SONNET_MODEL = modelSonnet.value;
|
||||||
|
if (modelOpus && modelOpus.value) currentObj.env.ANTHROPIC_DEFAULT_OPUS_MODEL = modelOpus.value;
|
||||||
|
|
||||||
|
// Direct mode fields
|
||||||
|
if (cliConfigMode === 'direct') {
|
||||||
|
var authToken = document.getElementById('cli-auth-token');
|
||||||
|
var baseUrl = document.getElementById('cli-base-url');
|
||||||
|
if (authToken && authToken.value) currentObj.env.ANTHROPIC_AUTH_TOKEN = authToken.value;
|
||||||
|
if (baseUrl && baseUrl.value) currentObj.env.ANTHROPIC_BASE_URL = baseUrl.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure DISABLE_AUTOUPDATER
|
||||||
|
currentObj.env.DISABLE_AUTOUPDATER = '1';
|
||||||
|
|
||||||
|
editor.value = JSON.stringify(currentObj, null, 2);
|
||||||
|
updateJsonLineNumbers();
|
||||||
|
validateCliJson();
|
||||||
|
}
|
||||||
|
window.syncFormToJson = syncFormToJson;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get settings from JSON editor (merges with form data)
|
||||||
|
*/
|
||||||
|
function getSettingsFromJsonEditor() {
|
||||||
|
var editor = document.getElementById('cli-json-editor');
|
||||||
|
if (!editor) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(editor.value);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Submit CLI Settings Form (handles both Provider and Direct modes)
|
* Submit CLI Settings Form (handles both Provider and Direct modes)
|
||||||
*/
|
*/
|
||||||
@@ -4214,6 +4438,33 @@ async function submitCliSettingsForm() {
|
|||||||
data.settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL = opusModel;
|
data.settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL = opusModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Merge additional settings from JSON editor
|
||||||
|
var jsonSettings = getSettingsFromJsonEditor();
|
||||||
|
if (jsonSettings) {
|
||||||
|
// Merge env variables (JSON editor values take precedence for non-core fields)
|
||||||
|
if (jsonSettings.env) {
|
||||||
|
for (var envKey in jsonSettings.env) {
|
||||||
|
// Skip core fields that are managed by form inputs
|
||||||
|
if (envKey === 'ANTHROPIC_AUTH_TOKEN' || envKey === 'ANTHROPIC_BASE_URL') {
|
||||||
|
// Only use JSON editor value if form field is empty
|
||||||
|
if (!data.settings.env[envKey] && jsonSettings.env[envKey] && !jsonSettings.env[envKey].endsWith('...')) {
|
||||||
|
data.settings.env[envKey] = jsonSettings.env[envKey];
|
||||||
|
}
|
||||||
|
} else if (!['ANTHROPIC_MODEL', 'ANTHROPIC_DEFAULT_HAIKU_MODEL', 'ANTHROPIC_DEFAULT_SONNET_MODEL', 'ANTHROPIC_DEFAULT_OPUS_MODEL'].includes(envKey)) {
|
||||||
|
// For non-model env vars, use JSON editor value
|
||||||
|
data.settings.env[envKey] = jsonSettings.env[envKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge non-env settings from JSON editor
|
||||||
|
for (var settingKey in jsonSettings) {
|
||||||
|
if (settingKey !== 'env' && settingKey !== 'configMode' && settingKey !== 'providerId') {
|
||||||
|
data.settings[settingKey] = jsonSettings[settingKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Set ID if editing
|
// Set ID if editing
|
||||||
if (id) {
|
if (id) {
|
||||||
data.id = id;
|
data.id = id;
|
||||||
|
|||||||
@@ -2540,7 +2540,15 @@ async function loadModelList() {
|
|||||||
// Show models for local backend
|
// Show models for local backend
|
||||||
if (embeddingBackend !== 'litellm') {
|
if (embeddingBackend !== 'litellm') {
|
||||||
var models = result.result.models;
|
var models = result.result.models;
|
||||||
models.forEach(function(model) {
|
var predefinedModels = models.filter(function(m) { return m.source !== 'discovered'; });
|
||||||
|
var discoveredModels = models.filter(function(m) { return m.source === 'discovered'; });
|
||||||
|
|
||||||
|
// Split predefined models into recommended and others
|
||||||
|
var recommendedModels = predefinedModels.filter(function(m) { return m.recommended; });
|
||||||
|
var otherModels = predefinedModels.filter(function(m) { return !m.recommended; });
|
||||||
|
|
||||||
|
// Helper function to render model card
|
||||||
|
function renderModelCard(model) {
|
||||||
var statusIcon = model.installed
|
var statusIcon = model.installed
|
||||||
? '<i data-lucide="check-circle" class="w-3.5 h-3.5 text-success"></i>'
|
? '<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>';
|
: '<i data-lucide="circle" class="w-3.5 h-3.5 text-muted"></i>';
|
||||||
@@ -2553,11 +2561,15 @@ async function loadModelList() {
|
|||||||
? '<button class="text-xs text-destructive hover:underline" onclick="deleteModel(\'' + model.profile + '\')">Delete</button>'
|
? '<button class="text-xs text-destructive hover:underline" onclick="deleteModel(\'' + model.profile + '\')">Delete</button>'
|
||||||
: '<button class="text-xs text-primary hover:underline" onclick="downloadModel(\'' + model.profile + '\')">Download</button>';
|
: '<button class="text-xs text-primary hover:underline" onclick="downloadModel(\'' + model.profile + '\')">Download</button>';
|
||||||
|
|
||||||
html +=
|
var recommendedBadge = model.recommended
|
||||||
'<div class="flex items-center justify-between p-2 bg-muted/30 rounded" id="model-' + model.profile + '">' +
|
? '<span class="text-[10px] px-1 py-0.5 bg-success/20 text-success rounded">Rec</span>'
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return '<div class="flex items-center justify-between p-2 bg-muted/30 rounded" id="model-' + model.profile + '">' +
|
||||||
'<div class="flex items-center gap-2">' +
|
'<div class="flex items-center gap-2">' +
|
||||||
statusIcon +
|
statusIcon +
|
||||||
'<span class="text-sm font-medium">' + model.profile + '</span>' +
|
'<span class="text-sm font-medium">' + model.profile + '</span>' +
|
||||||
|
recommendedBadge +
|
||||||
'<button class="text-muted-foreground hover:text-foreground p-0.5" onclick="copyToClipboard(\'' + escapeHtml(model.model_name) + '\')" title="' + escapeHtml(model.model_name) + '">' +
|
'<button class="text-muted-foreground hover:text-foreground p-0.5" onclick="copyToClipboard(\'' + escapeHtml(model.model_name) + '\')" title="' + escapeHtml(model.model_name) + '">' +
|
||||||
'<i data-lucide="copy" class="w-3 h-3"></i>' +
|
'<i data-lucide="copy" class="w-3 h-3"></i>' +
|
||||||
'</button>' +
|
'</button>' +
|
||||||
@@ -2568,7 +2580,108 @@ async function loadModelList() {
|
|||||||
actionBtn +
|
actionBtn +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
});
|
}
|
||||||
|
|
||||||
|
// Show recommended models (always visible)
|
||||||
|
if (recommendedModels.length > 0) {
|
||||||
|
html += '<div class="text-xs font-medium text-muted-foreground mb-1 mt-2 flex items-center gap-1">' +
|
||||||
|
'<i data-lucide="star" class="w-3 h-3"></i> Recommended Models (' + recommendedModels.length + ')</div>';
|
||||||
|
recommendedModels.forEach(function(model) {
|
||||||
|
html += renderModelCard(model);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show other models (collapsed by default)
|
||||||
|
if (otherModels.length > 0) {
|
||||||
|
html += '<div class="mt-3">' +
|
||||||
|
'<button onclick="toggleOtherModels()" class="text-xs font-medium text-muted-foreground mb-1 flex items-center gap-1 hover:text-foreground">' +
|
||||||
|
'<i data-lucide="chevron-right" class="w-3 h-3 transition-transform" id="otherModelsChevron"></i>' +
|
||||||
|
'Other Models (' + otherModels.length + ')' +
|
||||||
|
'</button>' +
|
||||||
|
'<div id="otherModelsContainer" class="hidden space-y-1">';
|
||||||
|
otherModels.forEach(function(model) {
|
||||||
|
html += renderModelCard(model);
|
||||||
|
});
|
||||||
|
html += '</div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show discovered models (user manually placed)
|
||||||
|
if (discoveredModels.length > 0) {
|
||||||
|
html += '<div class="text-xs font-medium text-muted-foreground mb-1 mt-3 flex items-center gap-1">' +
|
||||||
|
'<i data-lucide="folder-search" class="w-3 h-3"></i> Discovered Models</div>';
|
||||||
|
discoveredModels.forEach(function(model) {
|
||||||
|
var sizeText = model.actual_size_mb ? model.actual_size_mb.toFixed(0) + ' MB' : 'Unknown';
|
||||||
|
var safeProfile = model.profile.replace(/[^a-zA-Z0-9-_]/g, '-');
|
||||||
|
|
||||||
|
html +=
|
||||||
|
'<div class="flex items-center justify-between p-2 bg-amber-500/10 border border-amber-500/20 rounded" id="model-' + safeProfile + '">' +
|
||||||
|
'<div class="flex items-center gap-2">' +
|
||||||
|
'<i data-lucide="check-circle" class="w-3.5 h-3.5 text-amber-500"></i>' +
|
||||||
|
'<span class="text-sm font-medium">' + escapeHtml(model.model_name) + '</span>' +
|
||||||
|
'<span class="text-[10px] px-1 py-0.5 bg-amber-500/20 text-amber-600 rounded">Manual</span>' +
|
||||||
|
'<span class="text-xs text-muted-foreground">' + (model.dimensions || '?') + 'd</span>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="flex items-center gap-3">' +
|
||||||
|
'<span class="text-xs text-muted-foreground">' + sizeText + '</span>' +
|
||||||
|
'<button class="text-xs text-destructive hover:underline" onclick="deleteDiscoveredModel(\'' + escapeHtml(model.cache_path) + '\')">Delete</button>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show manual install guide
|
||||||
|
var guide = result.result.manual_install_guide;
|
||||||
|
if (guide) {
|
||||||
|
html += '<div class="mt-4 p-3 bg-blue-500/5 border border-blue-500/20 rounded">' +
|
||||||
|
'<div class="flex items-center gap-1 text-xs font-medium text-blue-600 mb-2">' +
|
||||||
|
'<i data-lucide="download" class="w-3 h-3"></i> Manual Model Installation' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="text-xs text-muted-foreground space-y-1">';
|
||||||
|
if (guide.steps) {
|
||||||
|
guide.steps.forEach(function(step) {
|
||||||
|
html += '<div>' + escapeHtml(step) + '</div>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (guide.example) {
|
||||||
|
html += '<div class="mt-2 font-mono text-[10px] bg-muted/50 p-1.5 rounded overflow-x-auto">' +
|
||||||
|
'<code>' + escapeHtml(guide.example) + '</code>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
// Show multi-platform paths
|
||||||
|
if (guide.paths) {
|
||||||
|
html += '<div class="mt-2 space-y-1">' +
|
||||||
|
'<div class="text-[10px] font-medium">Cache paths:</div>' +
|
||||||
|
'<div class="font-mono text-[10px] bg-muted/50 p-1.5 rounded space-y-0.5">';
|
||||||
|
if (guide.paths.windows) {
|
||||||
|
html += '<div><span class="text-muted-foreground">Windows:</span> ' + escapeHtml(guide.paths.windows) + '</div>';
|
||||||
|
}
|
||||||
|
if (guide.paths.linux) {
|
||||||
|
html += '<div><span class="text-muted-foreground">Linux:</span> ' + escapeHtml(guide.paths.linux) + '</div>';
|
||||||
|
}
|
||||||
|
if (guide.paths.macos) {
|
||||||
|
html += '<div><span class="text-muted-foreground">macOS:</span> ' + escapeHtml(guide.paths.macos) + '</div>';
|
||||||
|
}
|
||||||
|
html += '</div></div>';
|
||||||
|
}
|
||||||
|
html += '</div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom model download section
|
||||||
|
html += '<div class="mt-4 p-3 bg-green-500/5 border border-green-500/20 rounded">' +
|
||||||
|
'<div class="flex items-center gap-1 text-xs font-medium text-green-600 mb-2">' +
|
||||||
|
'<i data-lucide="plus-circle" class="w-3 h-3"></i> Download Custom Model' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="flex gap-2">' +
|
||||||
|
'<input type="text" id="customModelInput" placeholder="e.g., BAAI/bge-small-en-v1.5" ' +
|
||||||
|
'class="flex-1 text-xs px-2 py-1.5 border border-border rounded bg-background focus:border-primary focus:ring-1 focus:ring-primary outline-none" />' +
|
||||||
|
'<button onclick="downloadCustomModel()" class="text-xs px-3 py-1.5 bg-primary text-primary-foreground rounded hover:bg-primary/90">' +
|
||||||
|
'Download' +
|
||||||
|
'</button>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="text-[10px] text-muted-foreground mt-2">' +
|
||||||
|
'Enter any HuggingFace model name compatible with FastEmbed' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>';
|
||||||
} else {
|
} else {
|
||||||
// LiteLLM backend - show API info
|
// LiteLLM backend - show API info
|
||||||
html +=
|
html +=
|
||||||
@@ -2588,6 +2701,19 @@ async function loadModelList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle visibility of other (non-recommended) models
|
||||||
|
*/
|
||||||
|
function toggleOtherModels() {
|
||||||
|
var container = document.getElementById('otherModelsContainer');
|
||||||
|
var chevron = document.getElementById('otherModelsChevron');
|
||||||
|
if (container && chevron) {
|
||||||
|
var isHidden = container.classList.contains('hidden');
|
||||||
|
container.classList.toggle('hidden');
|
||||||
|
chevron.style.transform = isHidden ? 'rotate(90deg)' : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download model (simplified version)
|
* Download model (simplified version)
|
||||||
*/
|
*/
|
||||||
@@ -2675,6 +2801,82 @@ async function deleteModel(profile) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download a custom HuggingFace model by name
|
||||||
|
*/
|
||||||
|
async function downloadCustomModel() {
|
||||||
|
var input = document.getElementById('customModelInput');
|
||||||
|
if (!input) return;
|
||||||
|
|
||||||
|
var modelName = input.value.trim();
|
||||||
|
if (!modelName) {
|
||||||
|
showRefreshToast('Please enter a model name', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!modelName.includes('/')) {
|
||||||
|
showRefreshToast('Invalid format. Use: org/model-name', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable input and show loading
|
||||||
|
input.disabled = true;
|
||||||
|
var originalPlaceholder = input.placeholder;
|
||||||
|
input.placeholder = 'Downloading...';
|
||||||
|
input.value = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
var response = await fetch('/api/codexlens/models/download-custom', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ model_name: modelName, model_type: 'embedding' })
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
showRefreshToast('Custom model downloaded: ' + modelName, 'success');
|
||||||
|
loadModelList();
|
||||||
|
} else {
|
||||||
|
showRefreshToast('Download failed: ' + result.error, 'error');
|
||||||
|
input.disabled = false;
|
||||||
|
input.placeholder = originalPlaceholder;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showRefreshToast('Error: ' + err.message, 'error');
|
||||||
|
input.disabled = false;
|
||||||
|
input.placeholder = originalPlaceholder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a discovered (manually placed) model by its cache path
|
||||||
|
*/
|
||||||
|
async function deleteDiscoveredModel(cachePath) {
|
||||||
|
if (!confirm('Delete this manually placed model?\n\nPath: ' + cachePath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var response = await fetch('/api/codexlens/models/delete-path', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ cache_path: cachePath })
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
showRefreshToast('Model deleted successfully', 'success');
|
||||||
|
loadModelList();
|
||||||
|
} else {
|
||||||
|
showRefreshToast('Delete failed: ' + result.error, 'error');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showRefreshToast('Error: ' + err.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// RERANKER MODEL MANAGEMENT
|
// RERANKER MODEL MANAGEMENT
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
@@ -1995,6 +1995,55 @@ def model_delete(
|
|||||||
console.print(f" Freed space: {data['deleted_size_mb']:.1f} MB")
|
console.print(f" Freed space: {data['deleted_size_mb']:.1f} MB")
|
||||||
|
|
||||||
|
|
||||||
|
@app.command(name="model-download-custom")
|
||||||
|
def model_download_custom(
|
||||||
|
model_name: str = typer.Argument(..., help="Full HuggingFace model name (e.g., BAAI/bge-small-en-v1.5)."),
|
||||||
|
model_type: str = typer.Option("embedding", "--type", help="Model type: embedding or reranker."),
|
||||||
|
json_mode: bool = typer.Option(False, "--json", help="Output JSON response."),
|
||||||
|
) -> None:
|
||||||
|
"""Download a custom HuggingFace model by name.
|
||||||
|
|
||||||
|
This allows downloading any fastembed-compatible model from HuggingFace.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
codexlens model-download-custom BAAI/bge-small-en-v1.5
|
||||||
|
codexlens model-download-custom BAAI/bge-reranker-base --type reranker
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from codexlens.cli.model_manager import download_custom_model
|
||||||
|
|
||||||
|
if not json_mode:
|
||||||
|
console.print(f"[bold]Downloading custom model:[/bold] {model_name}")
|
||||||
|
console.print(f"[dim]Model type: {model_type}[/dim]")
|
||||||
|
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_custom_model(model_name, model_type=model_type, 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] Custom model downloaded successfully!")
|
||||||
|
console.print(f" Model: {data['model_name']}")
|
||||||
|
console.print(f" Type: {data['model_type']}")
|
||||||
|
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 not installed. Install with: pip install codexlens[semantic]")
|
||||||
|
else:
|
||||||
|
console.print("[red]Error:[/red] fastembed not installed")
|
||||||
|
console.print("[yellow]Install with:[/yellow] pip install codexlens[semantic]")
|
||||||
|
raise typer.Exit(code=1)
|
||||||
|
|
||||||
|
|
||||||
@app.command(name="model-info")
|
@app.command(name="model-info")
|
||||||
def model_info(
|
def model_info(
|
||||||
profile: str = typer.Argument(..., help="Model profile to get info (fast, code, multilingual, balanced)."),
|
profile: str = typer.Argument(..., help="Model profile to get info (fast, code, multilingual, balanced)."),
|
||||||
|
|||||||
@@ -76,6 +76,31 @@ RERANKER_MODEL_PROFILES = {
|
|||||||
"use_case": "Fast reranking with good accuracy",
|
"use_case": "Fast reranking with good accuracy",
|
||||||
"recommended": True,
|
"recommended": True,
|
||||||
},
|
},
|
||||||
|
# Additional reranker models (commonly used)
|
||||||
|
"bge-reranker-v2-m3": {
|
||||||
|
"model_name": "BAAI/bge-reranker-v2-m3",
|
||||||
|
"cache_name": "BAAI/bge-reranker-v2-m3",
|
||||||
|
"size_mb": 560,
|
||||||
|
"description": "BGE v2 M3 reranker, multilingual",
|
||||||
|
"use_case": "Multilingual reranking, latest BGE version",
|
||||||
|
"recommended": True,
|
||||||
|
},
|
||||||
|
"bge-reranker-v2-gemma": {
|
||||||
|
"model_name": "BAAI/bge-reranker-v2-gemma",
|
||||||
|
"cache_name": "BAAI/bge-reranker-v2-gemma",
|
||||||
|
"size_mb": 2000,
|
||||||
|
"description": "BGE v2 Gemma reranker, best quality",
|
||||||
|
"use_case": "Maximum quality with Gemma backbone",
|
||||||
|
"recommended": False,
|
||||||
|
},
|
||||||
|
"cross-encoder-ms-marco": {
|
||||||
|
"model_name": "cross-encoder/ms-marco-MiniLM-L-6-v2",
|
||||||
|
"cache_name": "cross-encoder/ms-marco-MiniLM-L-6-v2",
|
||||||
|
"size_mb": 90,
|
||||||
|
"description": "Original cross-encoder MS MARCO",
|
||||||
|
"use_case": "Classic cross-encoder baseline",
|
||||||
|
"recommended": False,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -138,6 +163,106 @@ MODEL_PROFILES = {
|
|||||||
"use_case": "High-quality semantic search, balanced performance",
|
"use_case": "High-quality semantic search, balanced performance",
|
||||||
"recommended": False, # 1024d not recommended
|
"recommended": False, # 1024d not recommended
|
||||||
},
|
},
|
||||||
|
# Additional embedding models (commonly used)
|
||||||
|
"bge-large": {
|
||||||
|
"model_name": "BAAI/bge-large-en-v1.5",
|
||||||
|
"cache_name": "qdrant/bge-large-en-v1.5-onnx-q",
|
||||||
|
"dimensions": 1024,
|
||||||
|
"size_mb": 650,
|
||||||
|
"description": "BGE large model, highest quality",
|
||||||
|
"use_case": "Maximum quality semantic search",
|
||||||
|
"recommended": False,
|
||||||
|
},
|
||||||
|
"e5-small": {
|
||||||
|
"model_name": "intfloat/e5-small-v2",
|
||||||
|
"cache_name": "qdrant/e5-small-v2-onnx",
|
||||||
|
"dimensions": 384,
|
||||||
|
"size_mb": 80,
|
||||||
|
"description": "E5 small model, fast and lightweight",
|
||||||
|
"use_case": "Low latency applications",
|
||||||
|
"recommended": True,
|
||||||
|
},
|
||||||
|
"e5-base": {
|
||||||
|
"model_name": "intfloat/e5-base-v2",
|
||||||
|
"cache_name": "qdrant/e5-base-v2-onnx",
|
||||||
|
"dimensions": 768,
|
||||||
|
"size_mb": 220,
|
||||||
|
"description": "E5 base model, balanced",
|
||||||
|
"use_case": "General purpose semantic search",
|
||||||
|
"recommended": True,
|
||||||
|
},
|
||||||
|
"e5-large": {
|
||||||
|
"model_name": "intfloat/e5-large-v2",
|
||||||
|
"cache_name": "qdrant/e5-large-v2-onnx",
|
||||||
|
"dimensions": 1024,
|
||||||
|
"size_mb": 650,
|
||||||
|
"description": "E5 large model, high quality",
|
||||||
|
"use_case": "High quality semantic search",
|
||||||
|
"recommended": False,
|
||||||
|
},
|
||||||
|
"jina-base-en": {
|
||||||
|
"model_name": "jinaai/jina-embeddings-v2-base-en",
|
||||||
|
"cache_name": "jinaai/jina-embeddings-v2-base-en",
|
||||||
|
"dimensions": 768,
|
||||||
|
"size_mb": 150,
|
||||||
|
"description": "Jina base English model",
|
||||||
|
"use_case": "English text semantic search",
|
||||||
|
"recommended": True,
|
||||||
|
},
|
||||||
|
"jina-small-en": {
|
||||||
|
"model_name": "jinaai/jina-embeddings-v2-small-en",
|
||||||
|
"cache_name": "jinaai/jina-embeddings-v2-small-en",
|
||||||
|
"dimensions": 512,
|
||||||
|
"size_mb": 60,
|
||||||
|
"description": "Jina small English model, very fast",
|
||||||
|
"use_case": "Low latency English text search",
|
||||||
|
"recommended": True,
|
||||||
|
},
|
||||||
|
"snowflake-arctic": {
|
||||||
|
"model_name": "Snowflake/snowflake-arctic-embed-m",
|
||||||
|
"cache_name": "Snowflake/snowflake-arctic-embed-m",
|
||||||
|
"dimensions": 768,
|
||||||
|
"size_mb": 220,
|
||||||
|
"description": "Snowflake Arctic embedding model",
|
||||||
|
"use_case": "Enterprise semantic search, high quality",
|
||||||
|
"recommended": True,
|
||||||
|
},
|
||||||
|
"nomic-embed": {
|
||||||
|
"model_name": "nomic-ai/nomic-embed-text-v1.5",
|
||||||
|
"cache_name": "nomic-ai/nomic-embed-text-v1.5",
|
||||||
|
"dimensions": 768,
|
||||||
|
"size_mb": 280,
|
||||||
|
"description": "Nomic embedding model, open source",
|
||||||
|
"use_case": "Open source text embedding",
|
||||||
|
"recommended": True,
|
||||||
|
},
|
||||||
|
"gte-small": {
|
||||||
|
"model_name": "thenlper/gte-small",
|
||||||
|
"cache_name": "thenlper/gte-small",
|
||||||
|
"dimensions": 384,
|
||||||
|
"size_mb": 70,
|
||||||
|
"description": "GTE small model, fast",
|
||||||
|
"use_case": "Fast text embedding",
|
||||||
|
"recommended": True,
|
||||||
|
},
|
||||||
|
"gte-base": {
|
||||||
|
"model_name": "thenlper/gte-base",
|
||||||
|
"cache_name": "thenlper/gte-base",
|
||||||
|
"dimensions": 768,
|
||||||
|
"size_mb": 220,
|
||||||
|
"description": "GTE base model, balanced",
|
||||||
|
"use_case": "General purpose text embedding",
|
||||||
|
"recommended": True,
|
||||||
|
},
|
||||||
|
"gte-large": {
|
||||||
|
"model_name": "thenlper/gte-large",
|
||||||
|
"cache_name": "thenlper/gte-large",
|
||||||
|
"dimensions": 1024,
|
||||||
|
"size_mb": 650,
|
||||||
|
"description": "GTE large model, high quality",
|
||||||
|
"use_case": "High quality text embedding",
|
||||||
|
"recommended": False,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -179,6 +304,92 @@ def _get_model_cache_path(cache_dir: Path, info: Dict) -> Path:
|
|||||||
return cache_dir / sanitized_name
|
return cache_dir / sanitized_name
|
||||||
|
|
||||||
|
|
||||||
|
def scan_discovered_models(model_type: str = "embedding") -> List[Dict]:
|
||||||
|
"""Scan cache directory for manually placed models not in predefined profiles.
|
||||||
|
|
||||||
|
This allows users to manually download models (e.g., via huggingface-cli or
|
||||||
|
by copying the model directory) and have them recognized automatically.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model_type: Type of models to scan for ("embedding" or "reranker")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of discovered model info dictionaries
|
||||||
|
"""
|
||||||
|
cache_dir = get_cache_dir()
|
||||||
|
if not cache_dir.exists():
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Get known model cache names based on type
|
||||||
|
if model_type == "reranker":
|
||||||
|
known_cache_names = {
|
||||||
|
f"models--{info.get('cache_name', info['model_name']).replace('/', '--')}"
|
||||||
|
for info in RERANKER_MODEL_PROFILES.values()
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
known_cache_names = {
|
||||||
|
f"models--{info.get('cache_name', info['model_name']).replace('/', '--')}"
|
||||||
|
for info in MODEL_PROFILES.values()
|
||||||
|
}
|
||||||
|
|
||||||
|
discovered = []
|
||||||
|
|
||||||
|
# Scan for model directories in cache
|
||||||
|
for item in cache_dir.iterdir():
|
||||||
|
if not item.is_dir() or not item.name.startswith("models--"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip known predefined models
|
||||||
|
if item.name in known_cache_names:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Parse model name from directory (models--org--model -> org/model)
|
||||||
|
parts = item.name[8:].split("--") # Remove "models--" prefix
|
||||||
|
if len(parts) >= 2:
|
||||||
|
model_name = "/".join(parts)
|
||||||
|
else:
|
||||||
|
model_name = parts[0] if parts else item.name
|
||||||
|
|
||||||
|
# Detect model type by checking for common patterns
|
||||||
|
is_reranker = any(keyword in model_name.lower() for keyword in [
|
||||||
|
"reranker", "cross-encoder", "ms-marco"
|
||||||
|
])
|
||||||
|
is_embedding = any(keyword in model_name.lower() for keyword in [
|
||||||
|
"embed", "bge", "e5", "jina", "minilm", "gte", "nomic", "arctic"
|
||||||
|
])
|
||||||
|
|
||||||
|
# Filter based on requested type
|
||||||
|
if model_type == "reranker" and not is_reranker:
|
||||||
|
continue
|
||||||
|
if model_type == "embedding" and is_reranker:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Calculate cache size
|
||||||
|
try:
|
||||||
|
total_size = sum(
|
||||||
|
f.stat().st_size
|
||||||
|
for f in item.rglob("*")
|
||||||
|
if f.is_file()
|
||||||
|
)
|
||||||
|
cache_size_mb = round(total_size / (1024 * 1024), 1)
|
||||||
|
except (OSError, PermissionError):
|
||||||
|
cache_size_mb = 0
|
||||||
|
|
||||||
|
discovered.append({
|
||||||
|
"profile": f"discovered:{model_name.replace('/', '-')}",
|
||||||
|
"model_name": model_name,
|
||||||
|
"cache_name": model_name,
|
||||||
|
"cache_path": str(item),
|
||||||
|
"actual_size_mb": cache_size_mb,
|
||||||
|
"description": f"Manually discovered model",
|
||||||
|
"use_case": "User-provided model",
|
||||||
|
"installed": True,
|
||||||
|
"source": "discovered", # Mark as discovered
|
||||||
|
})
|
||||||
|
|
||||||
|
return discovered
|
||||||
|
|
||||||
|
|
||||||
def list_models() -> Dict[str, any]:
|
def list_models() -> Dict[str, any]:
|
||||||
"""List available model profiles and their installation status.
|
"""List available model profiles and their installation status.
|
||||||
|
|
||||||
@@ -224,14 +435,45 @@ def list_models() -> Dict[str, any]:
|
|||||||
"description": info["description"],
|
"description": info["description"],
|
||||||
"use_case": info["use_case"],
|
"use_case": info["use_case"],
|
||||||
"installed": installed,
|
"installed": installed,
|
||||||
|
"source": "predefined", # Mark as predefined
|
||||||
|
"recommended": info.get("recommended", True),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Add discovered models (manually placed by user)
|
||||||
|
discovered = scan_discovered_models(model_type="embedding")
|
||||||
|
for model in discovered:
|
||||||
|
# Try to estimate dimensions based on common model patterns
|
||||||
|
dimensions = 768 # Default
|
||||||
|
name_lower = model["model_name"].lower()
|
||||||
|
if "small" in name_lower or "mini" in name_lower:
|
||||||
|
dimensions = 384
|
||||||
|
elif "large" in name_lower:
|
||||||
|
dimensions = 1024
|
||||||
|
|
||||||
|
model["dimensions"] = dimensions
|
||||||
|
model["estimated_size_mb"] = model.get("actual_size_mb", 0)
|
||||||
|
model["recommended"] = False # User-provided models are not recommended by default
|
||||||
|
models.append(model)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"result": {
|
"result": {
|
||||||
"models": models,
|
"models": models,
|
||||||
"cache_dir": str(cache_dir),
|
"cache_dir": str(cache_dir),
|
||||||
"cache_exists": cache_exists,
|
"cache_exists": cache_exists,
|
||||||
|
"manual_install_guide": {
|
||||||
|
"steps": [
|
||||||
|
"1. Download: huggingface-cli download <org>/<model>",
|
||||||
|
"2. Or copy to cache directory (see paths below)",
|
||||||
|
"3. Refresh to see discovered models"
|
||||||
|
],
|
||||||
|
"example": "huggingface-cli download BAAI/bge-small-en-v1.5",
|
||||||
|
"paths": {
|
||||||
|
"windows": "%USERPROFILE%\\.cache\\huggingface\\models--<org>--<model>",
|
||||||
|
"linux": "~/.cache/huggingface/models--<org>--<model>",
|
||||||
|
"macos": "~/.cache/huggingface/models--<org>--<model>",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,6 +555,92 @@ def download_model(profile: str, progress_callback: Optional[callable] = None) -
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def download_custom_model(model_name: str, model_type: str = "embedding", progress_callback: Optional[callable] = None) -> Dict[str, any]:
|
||||||
|
"""Download a custom model by HuggingFace model name.
|
||||||
|
|
||||||
|
This allows users to download any HuggingFace model that is compatible
|
||||||
|
with fastembed (TextEmbedding or TextCrossEncoder).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model_name: Full HuggingFace model name (e.g., "BAAI/bge-small-en-v1.5")
|
||||||
|
model_type: Type of model ("embedding" or "reranker")
|
||||||
|
progress_callback: Optional callback function to report progress
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Result dictionary with success status
|
||||||
|
"""
|
||||||
|
if model_type == "embedding":
|
||||||
|
if not FASTEMBED_AVAILABLE:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "fastembed not installed. Install with: pip install codexlens[semantic]",
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
if not RERANKER_AVAILABLE:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "fastembed reranker not available. Install with: pip install fastembed>=0.4.0",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validate model name format (org/model-name)
|
||||||
|
if not model_name or "/" not in model_name:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "Invalid model name format. Expected: 'org/model-name' (e.g., 'BAAI/bge-small-en-v1.5')",
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
cache_dir = get_cache_dir()
|
||||||
|
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(f"Downloading custom model {model_name}...")
|
||||||
|
|
||||||
|
if model_type == "reranker":
|
||||||
|
# Download reranker model
|
||||||
|
reranker = TextCrossEncoder(model_name=model_name, cache_dir=str(cache_dir))
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(f"Initializing reranker {model_name}...")
|
||||||
|
list(reranker.rerank("test query", ["test document"]))
|
||||||
|
else:
|
||||||
|
# Download embedding model
|
||||||
|
embedder = TextEmbedding(model_name=model_name, cache_dir=str(cache_dir))
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(f"Initializing {model_name}...")
|
||||||
|
list(embedder.embed(["test"]))
|
||||||
|
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(f"Custom model {model_name} downloaded successfully")
|
||||||
|
|
||||||
|
# Get cache info
|
||||||
|
sanitized_name = f"models--{model_name.replace('/', '--')}"
|
||||||
|
model_cache_path = cache_dir / sanitized_name
|
||||||
|
|
||||||
|
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": {
|
||||||
|
"model_name": model_name,
|
||||||
|
"model_type": model_type,
|
||||||
|
"cache_size_mb": cache_size,
|
||||||
|
"cache_path": str(model_cache_path),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Failed to download custom model: {str(e)}",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def delete_model(profile: str) -> Dict[str, any]:
|
def delete_model(profile: str) -> Dict[str, any]:
|
||||||
"""Delete a downloaded model from cache.
|
"""Delete a downloaded model from cache.
|
||||||
|
|
||||||
@@ -464,14 +792,35 @@ def list_reranker_models() -> Dict[str, any]:
|
|||||||
"use_case": info["use_case"],
|
"use_case": info["use_case"],
|
||||||
"installed": installed,
|
"installed": installed,
|
||||||
"recommended": info.get("recommended", True),
|
"recommended": info.get("recommended", True),
|
||||||
|
"source": "predefined", # Mark as predefined
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Add discovered reranker models (manually placed by user)
|
||||||
|
discovered = scan_discovered_models(model_type="reranker")
|
||||||
|
for model in discovered:
|
||||||
|
model["estimated_size_mb"] = model.get("actual_size_mb", 0)
|
||||||
|
model["recommended"] = False # User-provided models are not recommended by default
|
||||||
|
models.append(model)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"result": {
|
"result": {
|
||||||
"models": models,
|
"models": models,
|
||||||
"cache_dir": str(cache_dir),
|
"cache_dir": str(cache_dir),
|
||||||
"cache_exists": cache_exists,
|
"cache_exists": cache_exists,
|
||||||
|
"manual_install_guide": {
|
||||||
|
"steps": [
|
||||||
|
"1. Download: huggingface-cli download <org>/<model>",
|
||||||
|
"2. Or copy to cache directory (see paths below)",
|
||||||
|
"3. Refresh to see discovered models",
|
||||||
|
],
|
||||||
|
"example": "huggingface-cli download BAAI/bge-reranker-base",
|
||||||
|
"paths": {
|
||||||
|
"windows": "%USERPROFILE%\\.cache\\huggingface\\models--<org>--<model>",
|
||||||
|
"linux": "~/.cache/huggingface/models--<org>--<model>",
|
||||||
|
"macos": "~/.cache/huggingface/models--<org>--<model>",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user