mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +08:00
feat: 添加对 LiteLLM 嵌入后端的支持,增强并发 API 调用能力
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
bootstrapVenv,
|
||||
executeCodexLens,
|
||||
checkSemanticStatus,
|
||||
ensureLiteLLMEmbedderReady,
|
||||
installSemantic,
|
||||
detectGpuSupport,
|
||||
uninstallCodexLens,
|
||||
@@ -405,9 +406,17 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
|
||||
// API: CodexLens Init (Initialize workspace index)
|
||||
if (pathname === '/api/codexlens/init' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
const { path: projectPath, indexType = 'vector', embeddingModel = 'code', embeddingBackend = 'fastembed' } = body;
|
||||
const { path: projectPath, indexType = 'vector', embeddingModel = 'code', embeddingBackend = 'fastembed', maxWorkers = 1 } = body;
|
||||
const targetPath = projectPath || initialPath;
|
||||
|
||||
// Ensure LiteLLM backend dependencies are installed before running the CLI
|
||||
if (indexType !== 'normal' && embeddingBackend === 'litellm') {
|
||||
const installResult = await ensureLiteLLMEmbedderReady();
|
||||
if (!installResult.success) {
|
||||
return { success: false, error: installResult.error || 'Failed to prepare LiteLLM embedder', status: 500 };
|
||||
}
|
||||
}
|
||||
|
||||
// Build CLI arguments based on index type
|
||||
const args = ['init', targetPath, '--json'];
|
||||
if (indexType === 'normal') {
|
||||
@@ -419,6 +428,10 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
|
||||
if (embeddingBackend && embeddingBackend !== 'fastembed') {
|
||||
args.push('--embedding-backend', embeddingBackend);
|
||||
}
|
||||
// Add max workers for concurrent API calls (useful for litellm backend)
|
||||
if (maxWorkers && maxWorkers > 1) {
|
||||
args.push('--max-workers', String(maxWorkers));
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast start event
|
||||
|
||||
@@ -1167,14 +1167,17 @@ async function deleteModel(profile) {
|
||||
* @param {string} indexType - 'vector' (with embeddings), 'normal' (FTS only), or 'full' (FTS + Vector)
|
||||
* @param {string} embeddingModel - Model profile: 'code', 'fast'
|
||||
* @param {string} embeddingBackend - Backend: 'fastembed' (local) or 'litellm' (API)
|
||||
* @param {number} maxWorkers - Max concurrent API calls for embedding generation (default: 1)
|
||||
*/
|
||||
async function initCodexLensIndex(indexType, embeddingModel, embeddingBackend) {
|
||||
async function initCodexLensIndex(indexType, embeddingModel, embeddingBackend, maxWorkers) {
|
||||
indexType = indexType || 'vector';
|
||||
embeddingModel = embeddingModel || 'code';
|
||||
embeddingBackend = embeddingBackend || 'fastembed';
|
||||
maxWorkers = maxWorkers || 1;
|
||||
|
||||
// For vector or full index, check if semantic dependencies are available
|
||||
if (indexType === 'vector' || indexType === 'full') {
|
||||
// For vector/full index with local backend, check if semantic dependencies are available
|
||||
// LiteLLM backend uses remote embeddings and does not require fastembed/ONNX deps.
|
||||
if ((indexType === 'vector' || indexType === 'full') && embeddingBackend !== 'litellm') {
|
||||
try {
|
||||
var semanticResponse = await fetch('/api/codexlens/semantic/status');
|
||||
var semanticStatus = await semanticResponse.json();
|
||||
@@ -1275,7 +1278,7 @@ async function initCodexLensIndex(indexType, embeddingModel, embeddingBackend) {
|
||||
var apiIndexType = (indexType === 'full') ? 'vector' : indexType;
|
||||
|
||||
// Start indexing with specified type and model
|
||||
startCodexLensIndexing(apiIndexType, embeddingModel, embeddingBackend);
|
||||
startCodexLensIndexing(apiIndexType, embeddingModel, embeddingBackend, maxWorkers);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1283,11 +1286,13 @@ async function initCodexLensIndex(indexType, embeddingModel, embeddingBackend) {
|
||||
* @param {string} indexType - 'vector' or 'normal'
|
||||
* @param {string} embeddingModel - Model profile: 'code', 'fast'
|
||||
* @param {string} embeddingBackend - Backend: 'fastembed' (local) or 'litellm' (API)
|
||||
* @param {number} maxWorkers - Max concurrent API calls for embedding generation (default: 1)
|
||||
*/
|
||||
async function startCodexLensIndexing(indexType, embeddingModel, embeddingBackend) {
|
||||
async function startCodexLensIndexing(indexType, embeddingModel, embeddingBackend, maxWorkers) {
|
||||
indexType = indexType || 'vector';
|
||||
embeddingModel = embeddingModel || 'code';
|
||||
embeddingBackend = embeddingBackend || 'fastembed';
|
||||
maxWorkers = maxWorkers || 1;
|
||||
var statusText = document.getElementById('codexlensIndexStatus');
|
||||
var progressBar = document.getElementById('codexlensIndexProgressBar');
|
||||
var percentText = document.getElementById('codexlensIndexPercent');
|
||||
@@ -1319,11 +1324,11 @@ async function startCodexLensIndexing(indexType, embeddingModel, embeddingBacken
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('[CodexLens] Starting index for:', projectPath, 'type:', indexType, 'model:', embeddingModel, 'backend:', embeddingBackend);
|
||||
console.log('[CodexLens] Starting index for:', projectPath, 'type:', indexType, 'model:', embeddingModel, 'backend:', embeddingBackend, 'maxWorkers:', maxWorkers);
|
||||
var response = await fetch('/api/codexlens/init', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path: projectPath, indexType: indexType, embeddingModel: embeddingModel, embeddingBackend: embeddingBackend })
|
||||
body: JSON.stringify({ path: projectPath, indexType: indexType, embeddingModel: embeddingModel, embeddingBackend: embeddingBackend, maxWorkers: maxWorkers })
|
||||
});
|
||||
|
||||
var result = await response.json();
|
||||
@@ -1992,6 +1997,17 @@ function buildCodexLensManagerPage(config) {
|
||||
'</select>' +
|
||||
'<p class="text-xs text-muted-foreground mt-1">' + t('codexlens.modelHint') + '</p>' +
|
||||
'</div>' +
|
||||
// Concurrency selector (only for LiteLLM backend)
|
||||
'<div id="concurrencySelector" class="hidden">' +
|
||||
'<label class="block text-sm font-medium mb-1.5">' + (t('codexlens.concurrency') || 'API Concurrency') + '</label>' +
|
||||
'<select id="pageConcurrencySelect" class="w-full px-3 py-2 border border-border rounded-lg bg-background text-sm">' +
|
||||
'<option value="1">1 (Sequential)</option>' +
|
||||
'<option value="2">2 workers</option>' +
|
||||
'<option value="4" selected>4 workers (Recommended)</option>' +
|
||||
'<option value="8">8 workers</option>' +
|
||||
'</select>' +
|
||||
'<p class="text-xs text-muted-foreground mt-1">' + (t('codexlens.concurrencyHint') || 'Number of parallel API calls for embedding generation') + '</p>' +
|
||||
'</div>' +
|
||||
// Index buttons - two modes: full (FTS + Vector) or FTS only
|
||||
'<div class="grid grid-cols-2 gap-3">' +
|
||||
'<button class="btn btn-primary flex items-center justify-center gap-2 py-3" onclick="initCodexLensIndexFromPage(\'full\')" title="' + t('codexlens.fullIndexDesc') + '">' +
|
||||
@@ -2194,6 +2210,7 @@ function buildModelSelectOptionsForPage() {
|
||||
function onEmbeddingBackendChange() {
|
||||
var backendSelect = document.getElementById('pageBackendSelect');
|
||||
var modelSelect = document.getElementById('pageModelSelect');
|
||||
var concurrencySelector = document.getElementById('concurrencySelector');
|
||||
if (!backendSelect || !modelSelect) {
|
||||
console.warn('[CodexLens] Backend or model select not found');
|
||||
return;
|
||||
@@ -2209,9 +2226,17 @@ function onEmbeddingBackendChange() {
|
||||
var options = buildLiteLLMModelOptions();
|
||||
console.log('[CodexLens] Built options HTML:', options);
|
||||
modelSelect.innerHTML = options;
|
||||
// Show concurrency selector for API backend
|
||||
if (concurrencySelector) {
|
||||
concurrencySelector.classList.remove('hidden');
|
||||
}
|
||||
} else {
|
||||
// Load local fastembed models
|
||||
modelSelect.innerHTML = buildModelSelectOptionsForPage();
|
||||
// Hide concurrency selector for local backend
|
||||
if (concurrencySelector) {
|
||||
concurrencySelector.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2265,14 +2290,18 @@ window.onEmbeddingBackendChange = onEmbeddingBackendChange;
|
||||
function initCodexLensIndexFromPage(indexType) {
|
||||
var backendSelect = document.getElementById('pageBackendSelect');
|
||||
var modelSelect = document.getElementById('pageModelSelect');
|
||||
var concurrencySelect = document.getElementById('pageConcurrencySelect');
|
||||
var selectedBackend = backendSelect ? backendSelect.value : 'fastembed';
|
||||
var selectedModel = modelSelect ? modelSelect.value : 'code';
|
||||
var selectedConcurrency = concurrencySelect ? parseInt(concurrencySelect.value, 10) : 1;
|
||||
|
||||
// For FTS-only index, model is not needed
|
||||
if (indexType === 'normal') {
|
||||
initCodexLensIndex(indexType);
|
||||
} else {
|
||||
initCodexLensIndex(indexType, selectedModel, selectedBackend);
|
||||
// Pass concurrency only for litellm backend
|
||||
var maxWorkers = selectedBackend === 'litellm' ? selectedConcurrency : 1;
|
||||
initCodexLensIndex(indexType, selectedModel, selectedBackend, maxWorkers);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ interface SemanticStatus {
|
||||
backend?: string;
|
||||
accelerator?: string;
|
||||
providers?: string[];
|
||||
litellmAvailable?: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@@ -195,11 +196,18 @@ async function checkSemanticStatus(): Promise<SemanticStatus> {
|
||||
// Check semantic module availability and accelerator info
|
||||
return new Promise((resolve) => {
|
||||
const checkCode = `
|
||||
import sys
|
||||
import json
|
||||
try:
|
||||
from codexlens.semantic import SEMANTIC_AVAILABLE, SEMANTIC_BACKEND
|
||||
result = {"available": SEMANTIC_AVAILABLE, "backend": SEMANTIC_BACKEND if SEMANTIC_AVAILABLE else None}
|
||||
import sys
|
||||
import json
|
||||
try:
|
||||
import codexlens.semantic as semantic
|
||||
SEMANTIC_AVAILABLE = bool(getattr(semantic, "SEMANTIC_AVAILABLE", False))
|
||||
SEMANTIC_BACKEND = getattr(semantic, "SEMANTIC_BACKEND", None)
|
||||
LITELLM_AVAILABLE = bool(getattr(semantic, "LITELLM_AVAILABLE", False))
|
||||
result = {
|
||||
"available": SEMANTIC_AVAILABLE,
|
||||
"backend": SEMANTIC_BACKEND if SEMANTIC_AVAILABLE else None,
|
||||
"litellm_available": LITELLM_AVAILABLE,
|
||||
}
|
||||
|
||||
# Get ONNX providers for accelerator info
|
||||
try:
|
||||
@@ -250,6 +258,7 @@ except Exception as e:
|
||||
backend: result.backend,
|
||||
accelerator: result.accelerator || 'CPU',
|
||||
providers: result.providers || [],
|
||||
litellmAvailable: result.litellm_available || false,
|
||||
error: result.error
|
||||
});
|
||||
} catch {
|
||||
@@ -263,6 +272,77 @@ except Exception as e:
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure LiteLLM embedder dependencies are available in the CodexLens venv.
|
||||
* Installs ccw-litellm into the venv if needed.
|
||||
*/
|
||||
async function ensureLiteLLMEmbedderReady(): Promise<BootstrapResult> {
|
||||
// Ensure CodexLens venv exists and CodexLens is installed.
|
||||
const readyStatus = await ensureReady();
|
||||
if (!readyStatus.ready) {
|
||||
return { success: false, error: readyStatus.error || 'CodexLens not ready' };
|
||||
}
|
||||
|
||||
// Check if ccw_litellm can be imported
|
||||
const importStatus = await new Promise<{ ok: boolean; error?: string }>((resolve) => {
|
||||
const child = spawn(VENV_PYTHON, ['-c', 'import ccw_litellm; print("OK")'], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: 15000,
|
||||
});
|
||||
|
||||
let stderr = '';
|
||||
child.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
resolve({ ok: code === 0, error: stderr.trim() || undefined });
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
resolve({ ok: false, error: err.message });
|
||||
});
|
||||
});
|
||||
|
||||
if (importStatus.ok) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
const pipPath =
|
||||
process.platform === 'win32'
|
||||
? join(CODEXLENS_VENV, 'Scripts', 'pip.exe')
|
||||
: join(CODEXLENS_VENV, 'bin', 'pip');
|
||||
|
||||
try {
|
||||
console.log('[CodexLens] Installing ccw-litellm for LiteLLM embedding backend...');
|
||||
|
||||
const possiblePaths = [
|
||||
join(process.cwd(), 'ccw-litellm'),
|
||||
join(__dirname, '..', '..', '..', 'ccw-litellm'), // ccw/src/tools -> project root
|
||||
join(homedir(), 'ccw-litellm'),
|
||||
];
|
||||
|
||||
let installed = false;
|
||||
for (const localPath of possiblePaths) {
|
||||
if (existsSync(join(localPath, 'pyproject.toml'))) {
|
||||
console.log(`[CodexLens] Installing ccw-litellm from local path: ${localPath}`);
|
||||
execSync(`"${pipPath}" install -e "${localPath}"`, { stdio: 'inherit' });
|
||||
installed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!installed) {
|
||||
console.log('[CodexLens] Installing ccw-litellm from PyPI...');
|
||||
execSync(`"${pipPath}" install ccw-litellm`, { stdio: 'inherit' });
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
return { success: false, error: `Failed to install ccw-litellm: ${(err as Error).message}` };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GPU acceleration mode for semantic search
|
||||
*/
|
||||
@@ -1284,7 +1364,19 @@ function isIndexingInProgress(): boolean {
|
||||
export type { ProgressInfo, ExecuteOptions };
|
||||
|
||||
// Export for direct usage
|
||||
export { ensureReady, executeCodexLens, checkVenvStatus, bootstrapVenv, checkSemanticStatus, installSemantic, detectGpuSupport, uninstallCodexLens, cancelIndexing, isIndexingInProgress };
|
||||
export {
|
||||
ensureReady,
|
||||
executeCodexLens,
|
||||
checkVenvStatus,
|
||||
bootstrapVenv,
|
||||
checkSemanticStatus,
|
||||
ensureLiteLLMEmbedderReady,
|
||||
installSemantic,
|
||||
detectGpuSupport,
|
||||
uninstallCodexLens,
|
||||
cancelIndexing,
|
||||
isIndexingInProgress,
|
||||
};
|
||||
export type { GpuMode };
|
||||
|
||||
// Backward-compatible export for tests
|
||||
|
||||
Reference in New Issue
Block a user