mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
feat: Implement CodexLens multi-provider embedding rotation management
- Added functions to get and update CodexLens embedding rotation configuration. - Introduced functionality to retrieve enabled embedding providers for rotation. - Created endpoints for managing rotation configuration via API. - Enhanced dashboard UI to support multi-provider rotation configuration. - Updated internationalization strings for new rotation features. - Adjusted CLI commands and embedding manager to support increased concurrency limits. - Modified hybrid search weights for improved ranking behavior.
This commit is contained in:
@@ -13,6 +13,8 @@ import type {
|
||||
GlobalCacheSettings,
|
||||
ProviderType,
|
||||
CacheStrategy,
|
||||
CodexLensEmbeddingRotation,
|
||||
CodexLensEmbeddingProvider,
|
||||
} from '../types/litellm-api-config.js';
|
||||
|
||||
/**
|
||||
@@ -356,6 +358,199 @@ export function updateGlobalCacheSettings(
|
||||
saveConfig(baseDir, config);
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// CodexLens Embedding Rotation Management
|
||||
// ===========================
|
||||
|
||||
/**
|
||||
* Get CodexLens embedding rotation config
|
||||
*/
|
||||
export function getCodexLensEmbeddingRotation(baseDir: string): CodexLensEmbeddingRotation | undefined {
|
||||
const config = loadLiteLLMApiConfig(baseDir);
|
||||
return config.codexlensEmbeddingRotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update CodexLens embedding rotation config
|
||||
*/
|
||||
export function updateCodexLensEmbeddingRotation(
|
||||
baseDir: string,
|
||||
rotationConfig: CodexLensEmbeddingRotation | undefined
|
||||
): void {
|
||||
const config = loadLiteLLMApiConfig(baseDir);
|
||||
|
||||
if (rotationConfig) {
|
||||
config.codexlensEmbeddingRotation = rotationConfig;
|
||||
} else {
|
||||
delete config.codexlensEmbeddingRotation;
|
||||
}
|
||||
|
||||
saveConfig(baseDir, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all enabled embedding providers with their API keys for rotation
|
||||
* This aggregates all providers that have embedding models configured
|
||||
*/
|
||||
export function getEmbeddingProvidersForRotation(baseDir: string): Array<{
|
||||
providerId: string;
|
||||
providerName: string;
|
||||
apiBase: string;
|
||||
embeddingModels: Array<{
|
||||
modelId: string;
|
||||
modelName: string;
|
||||
dimensions: number;
|
||||
}>;
|
||||
apiKeys: Array<{
|
||||
keyId: string;
|
||||
keyLabel: string;
|
||||
enabled: boolean;
|
||||
}>;
|
||||
}> {
|
||||
const config = loadLiteLLMApiConfig(baseDir);
|
||||
const result: Array<{
|
||||
providerId: string;
|
||||
providerName: string;
|
||||
apiBase: string;
|
||||
embeddingModels: Array<{
|
||||
modelId: string;
|
||||
modelName: string;
|
||||
dimensions: number;
|
||||
}>;
|
||||
apiKeys: Array<{
|
||||
keyId: string;
|
||||
keyLabel: string;
|
||||
enabled: boolean;
|
||||
}>;
|
||||
}> = [];
|
||||
|
||||
for (const provider of config.providers) {
|
||||
if (!provider.enabled) continue;
|
||||
|
||||
// Check if provider has embedding models
|
||||
const embeddingModels = (provider.embeddingModels || [])
|
||||
.filter(m => m.enabled)
|
||||
.map(m => ({
|
||||
modelId: m.id,
|
||||
modelName: m.name,
|
||||
dimensions: m.capabilities?.embeddingDimension || 1536,
|
||||
}));
|
||||
|
||||
if (embeddingModels.length === 0) continue;
|
||||
|
||||
// Get API keys (single key or multiple from apiKeys array)
|
||||
const apiKeys: Array<{ keyId: string; keyLabel: string; enabled: boolean }> = [];
|
||||
|
||||
if (provider.apiKeys && provider.apiKeys.length > 0) {
|
||||
// Use multi-key configuration
|
||||
for (const keyEntry of provider.apiKeys) {
|
||||
apiKeys.push({
|
||||
keyId: keyEntry.id,
|
||||
keyLabel: keyEntry.label || keyEntry.id,
|
||||
enabled: keyEntry.enabled,
|
||||
});
|
||||
}
|
||||
} else if (provider.apiKey) {
|
||||
// Single key fallback
|
||||
apiKeys.push({
|
||||
keyId: 'default',
|
||||
keyLabel: 'Default Key',
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
|
||||
result.push({
|
||||
providerId: provider.id,
|
||||
providerName: provider.name,
|
||||
apiBase: provider.apiBase || getDefaultApiBaseForType(provider.type),
|
||||
embeddingModels,
|
||||
apiKeys,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate rotation endpoints for ccw_litellm
|
||||
* Creates endpoint list from rotation config for parallel embedding
|
||||
*/
|
||||
export function generateRotationEndpoints(baseDir: string): Array<{
|
||||
name: string;
|
||||
api_key: string;
|
||||
api_base: string;
|
||||
model: string;
|
||||
weight: number;
|
||||
max_concurrent: number;
|
||||
}> {
|
||||
const config = loadLiteLLMApiConfig(baseDir);
|
||||
const rotationConfig = config.codexlensEmbeddingRotation;
|
||||
|
||||
if (!rotationConfig || !rotationConfig.enabled) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const endpoints: Array<{
|
||||
name: string;
|
||||
api_key: string;
|
||||
api_base: string;
|
||||
model: string;
|
||||
weight: number;
|
||||
max_concurrent: number;
|
||||
}> = [];
|
||||
|
||||
for (const rotationProvider of rotationConfig.providers) {
|
||||
if (!rotationProvider.enabled) continue;
|
||||
|
||||
// Find the provider config
|
||||
const provider = config.providers.find(p => p.id === rotationProvider.providerId);
|
||||
if (!provider || !provider.enabled) continue;
|
||||
|
||||
// Find the embedding model
|
||||
const embeddingModel = provider.embeddingModels?.find(m => m.id === rotationProvider.modelId);
|
||||
if (!embeddingModel || !embeddingModel.enabled) continue;
|
||||
|
||||
// Get API base (model-specific or provider default)
|
||||
const apiBase = embeddingModel.endpointSettings?.baseUrl ||
|
||||
provider.apiBase ||
|
||||
getDefaultApiBaseForType(provider.type);
|
||||
|
||||
// Get API keys to use
|
||||
let keysToUse: Array<{ id: string; key: string; label: string }> = [];
|
||||
|
||||
if (provider.apiKeys && provider.apiKeys.length > 0) {
|
||||
if (rotationProvider.useAllKeys) {
|
||||
// Use all enabled keys
|
||||
keysToUse = provider.apiKeys
|
||||
.filter(k => k.enabled)
|
||||
.map(k => ({ id: k.id, key: k.key, label: k.label || k.id }));
|
||||
} else if (rotationProvider.selectedKeyIds && rotationProvider.selectedKeyIds.length > 0) {
|
||||
// Use only selected keys
|
||||
keysToUse = provider.apiKeys
|
||||
.filter(k => k.enabled && rotationProvider.selectedKeyIds!.includes(k.id))
|
||||
.map(k => ({ id: k.id, key: k.key, label: k.label || k.id }));
|
||||
}
|
||||
} else if (provider.apiKey) {
|
||||
// Single key fallback
|
||||
keysToUse = [{ id: 'default', key: provider.apiKey, label: 'Default' }];
|
||||
}
|
||||
|
||||
// Create endpoint for each key
|
||||
for (const keyInfo of keysToUse) {
|
||||
endpoints.push({
|
||||
name: `${provider.name}-${keyInfo.label}`,
|
||||
api_key: resolveEnvVar(keyInfo.key),
|
||||
api_base: apiBase,
|
||||
model: embeddingModel.name,
|
||||
weight: rotationProvider.weight,
|
||||
max_concurrent: rotationProvider.maxConcurrentPerKey,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// YAML Config Generation for ccw_litellm
|
||||
// ===========================
|
||||
@@ -518,4 +713,4 @@ function objectToYaml(obj: unknown, indent: number = 0): string {
|
||||
}
|
||||
|
||||
// Re-export types
|
||||
export type { ProviderCredential, CustomEndpoint, ProviderType, CacheStrategy };
|
||||
export type { ProviderCredential, CustomEndpoint, ProviderType, CacheStrategy, CodexLensEmbeddingRotation, CodexLensEmbeddingProvider };
|
||||
|
||||
@@ -22,9 +22,14 @@ import {
|
||||
loadLiteLLMApiConfig,
|
||||
saveLiteLLMYamlConfig,
|
||||
generateLiteLLMYamlConfig,
|
||||
getCodexLensEmbeddingRotation,
|
||||
updateCodexLensEmbeddingRotation,
|
||||
getEmbeddingProvidersForRotation,
|
||||
generateRotationEndpoints,
|
||||
type ProviderCredential,
|
||||
type CustomEndpoint,
|
||||
type ProviderType,
|
||||
type CodexLensEmbeddingRotation,
|
||||
} from '../../config/litellm-api-config-manager.js';
|
||||
import { getContextCacheStore } from '../../tools/context-cache-store.js';
|
||||
import { getLiteLLMClient } from '../../tools/litellm-client.js';
|
||||
@@ -568,6 +573,66 @@ export async function handleLiteLLMApiRoutes(ctx: RouteContext): Promise<boolean
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// CodexLens Embedding Rotation Routes
|
||||
// ===========================
|
||||
|
||||
// GET /api/litellm-api/codexlens/rotation - Get rotation config
|
||||
if (pathname === '/api/litellm-api/codexlens/rotation' && req.method === 'GET') {
|
||||
try {
|
||||
const rotationConfig = getCodexLensEmbeddingRotation(initialPath);
|
||||
const availableProviders = getEmbeddingProvidersForRotation(initialPath);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
rotationConfig: rotationConfig || null,
|
||||
availableProviders,
|
||||
}));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// PUT /api/litellm-api/codexlens/rotation - Update rotation config
|
||||
if (pathname === '/api/litellm-api/codexlens/rotation' && req.method === 'PUT') {
|
||||
handlePostRequest(req, res, async (body: unknown) => {
|
||||
const rotationConfig = body as CodexLensEmbeddingRotation | null;
|
||||
|
||||
try {
|
||||
updateCodexLensEmbeddingRotation(initialPath, rotationConfig || undefined);
|
||||
|
||||
broadcastToClients({
|
||||
type: 'CODEXLENS_ROTATION_UPDATED',
|
||||
payload: { rotationConfig, timestamp: new Date().toISOString() }
|
||||
});
|
||||
|
||||
return { success: true, rotationConfig };
|
||||
} catch (err) {
|
||||
return { error: (err as Error).message, status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// GET /api/litellm-api/codexlens/rotation/endpoints - Get generated rotation endpoints
|
||||
if (pathname === '/api/litellm-api/codexlens/rotation/endpoints' && req.method === 'GET') {
|
||||
try {
|
||||
const endpoints = generateRotationEndpoints(initialPath);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
endpoints,
|
||||
count: endpoints.length,
|
||||
}));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /api/litellm-api/ccw-litellm/install - Install ccw-litellm package
|
||||
if (pathname === '/api/litellm-api/ccw-litellm/install' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async () => {
|
||||
|
||||
@@ -269,6 +269,28 @@ const i18n = {
|
||||
'codexlens.concurrency': 'API Concurrency',
|
||||
'codexlens.concurrencyHint': 'Number of parallel API calls (1-32). Higher values speed up indexing but may hit rate limits.',
|
||||
'codexlens.concurrencyCustom': 'Custom',
|
||||
'codexlens.rotation': 'Multi-Provider Rotation',
|
||||
'codexlens.rotationDesc': 'Aggregate multiple API providers and keys for parallel embedding generation',
|
||||
'codexlens.rotationEnabled': 'Enable Rotation',
|
||||
'codexlens.rotationStrategy': 'Rotation Strategy',
|
||||
'codexlens.strategyRoundRobin': 'Round Robin',
|
||||
'codexlens.strategyLatencyAware': 'Latency Aware',
|
||||
'codexlens.strategyWeightedRandom': 'Weighted Random',
|
||||
'codexlens.targetModel': 'Target Model',
|
||||
'codexlens.targetModelHint': 'Model name that all providers should support (e.g., qwen3-embedding)',
|
||||
'codexlens.cooldownSeconds': 'Cooldown (seconds)',
|
||||
'codexlens.cooldownHint': 'Default cooldown after rate limit (60s recommended)',
|
||||
'codexlens.rotationProviders': 'Rotation Providers',
|
||||
'codexlens.addProvider': 'Add Provider',
|
||||
'codexlens.noRotationProviders': 'No providers configured for rotation',
|
||||
'codexlens.providerWeight': 'Weight',
|
||||
'codexlens.maxConcurrentPerKey': 'Max Concurrent/Key',
|
||||
'codexlens.useAllKeys': 'Use All Keys',
|
||||
'codexlens.selectKeys': 'Select Keys',
|
||||
'codexlens.configureRotation': 'Configure Rotation',
|
||||
'codexlens.rotationSaved': 'Rotation config saved successfully',
|
||||
'codexlens.rotationDeleted': 'Rotation config deleted',
|
||||
'codexlens.totalEndpoints': 'Total Endpoints',
|
||||
'codexlens.fullIndex': 'Full',
|
||||
'codexlens.vectorIndex': 'Vector',
|
||||
'codexlens.ftsIndex': 'FTS',
|
||||
@@ -1931,6 +1953,28 @@ const i18n = {
|
||||
'codexlens.concurrency': 'API 并发数',
|
||||
'codexlens.concurrencyHint': '并行 API 调用数量(1-32)。较高的值可加速索引但可能触发速率限制。',
|
||||
'codexlens.concurrencyCustom': '自定义',
|
||||
'codexlens.rotation': '多供应商轮训',
|
||||
'codexlens.rotationDesc': '聚合多个 API 供应商和密钥进行并行嵌入生成',
|
||||
'codexlens.rotationEnabled': '启用轮训',
|
||||
'codexlens.rotationStrategy': '轮训策略',
|
||||
'codexlens.strategyRoundRobin': '轮询',
|
||||
'codexlens.strategyLatencyAware': '延迟感知',
|
||||
'codexlens.strategyWeightedRandom': '加权随机',
|
||||
'codexlens.targetModel': '目标模型',
|
||||
'codexlens.targetModelHint': '所有供应商应支持的模型名称(例如 qwen3-embedding)',
|
||||
'codexlens.cooldownSeconds': '冷却时间(秒)',
|
||||
'codexlens.cooldownHint': '速率限制后的默认冷却时间(推荐 60 秒)',
|
||||
'codexlens.rotationProviders': '轮训供应商',
|
||||
'codexlens.addProvider': '添加供应商',
|
||||
'codexlens.noRotationProviders': '未配置轮训供应商',
|
||||
'codexlens.providerWeight': '权重',
|
||||
'codexlens.maxConcurrentPerKey': '每密钥最大并发',
|
||||
'codexlens.useAllKeys': '使用所有密钥',
|
||||
'codexlens.selectKeys': '选择密钥',
|
||||
'codexlens.configureRotation': '配置轮训',
|
||||
'codexlens.rotationSaved': '轮训配置保存成功',
|
||||
'codexlens.rotationDeleted': '轮训配置已删除',
|
||||
'codexlens.totalEndpoints': '总端点数',
|
||||
'codexlens.fullIndex': '全部',
|
||||
'codexlens.vectorIndex': '向量',
|
||||
'codexlens.ftsIndex': 'FTS',
|
||||
|
||||
@@ -2009,6 +2009,28 @@ function buildCodexLensManagerPage(config) {
|
||||
'</div>' +
|
||||
'<p class="text-xs text-muted-foreground mt-1">' + t('codexlens.concurrencyHint') + '</p>' +
|
||||
'</div>' +
|
||||
// Multi-Provider Rotation (only for LiteLLM backend)
|
||||
'<div id="rotationSection" class="hidden">' +
|
||||
'<div class="border border-border rounded-lg p-3 bg-muted/30">' +
|
||||
'<div class="flex items-center justify-between mb-2">' +
|
||||
'<div class="flex items-center gap-2">' +
|
||||
'<i data-lucide="rotate-cw" class="w-4 h-4 text-primary"></i>' +
|
||||
'<span class="text-sm font-medium">' + t('codexlens.rotation') + '</span>' +
|
||||
'</div>' +
|
||||
'<div id="rotationStatusBadge" class="text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground">' +
|
||||
t('common.disabled') +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<p class="text-xs text-muted-foreground mb-3">' + t('codexlens.rotationDesc') + '</p>' +
|
||||
'<div class="flex items-center gap-2">' +
|
||||
'<button class="btn-sm btn-outline flex items-center gap-1.5" onclick="showRotationConfigModal()">' +
|
||||
'<i data-lucide="settings" class="w-3.5 h-3.5"></i>' +
|
||||
t('codexlens.configureRotation') +
|
||||
'</button>' +
|
||||
'<span id="rotationEndpointCount" class="text-xs text-muted-foreground"></span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</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') + '">' +
|
||||
@@ -2224,6 +2246,7 @@ function onEmbeddingBackendChange() {
|
||||
var backendSelect = document.getElementById('pageBackendSelect');
|
||||
var modelSelect = document.getElementById('pageModelSelect');
|
||||
var concurrencySelector = document.getElementById('concurrencySelector');
|
||||
var rotationSection = document.getElementById('rotationSection');
|
||||
if (!backendSelect || !modelSelect) {
|
||||
console.warn('[CodexLens] Backend or model select not found');
|
||||
return;
|
||||
@@ -2243,6 +2266,11 @@ function onEmbeddingBackendChange() {
|
||||
if (concurrencySelector) {
|
||||
concurrencySelector.classList.remove('hidden');
|
||||
}
|
||||
// Show rotation section and load status
|
||||
if (rotationSection) {
|
||||
rotationSection.classList.remove('hidden');
|
||||
loadRotationStatus();
|
||||
}
|
||||
} else {
|
||||
// Load local fastembed models
|
||||
modelSelect.innerHTML = buildModelSelectOptionsForPage();
|
||||
@@ -2250,6 +2278,10 @@ function onEmbeddingBackendChange() {
|
||||
if (concurrencySelector) {
|
||||
concurrencySelector.classList.add('hidden');
|
||||
}
|
||||
// Hide rotation section for local backend
|
||||
if (rotationSection) {
|
||||
rotationSection.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2553,3 +2585,308 @@ async function cleanAllIndexesFromPage() {
|
||||
showRefreshToast((t('common.error') || 'Error') + ': ' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// MULTI-PROVIDER ROTATION CONFIGURATION
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Load and display rotation status in the page
|
||||
*/
|
||||
async function loadRotationStatus() {
|
||||
try {
|
||||
var response = await fetch('/api/litellm-api/codexlens/rotation');
|
||||
if (!response.ok) {
|
||||
console.warn('[CodexLens] Failed to load rotation config:', response.status);
|
||||
return;
|
||||
}
|
||||
var data = await response.json();
|
||||
window.rotationConfig = data.rotationConfig;
|
||||
window.availableRotationProviders = data.availableProviders;
|
||||
updateRotationStatusDisplay(data.rotationConfig);
|
||||
} catch (err) {
|
||||
console.error('[CodexLens] Error loading rotation status:', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the rotation status display in the page
|
||||
*/
|
||||
function updateRotationStatusDisplay(rotationConfig) {
|
||||
var badge = document.getElementById('rotationStatusBadge');
|
||||
var countEl = document.getElementById('rotationEndpointCount');
|
||||
|
||||
if (!badge) return;
|
||||
|
||||
if (rotationConfig && rotationConfig.enabled) {
|
||||
badge.textContent = t('common.enabled');
|
||||
badge.className = 'text-xs px-2 py-0.5 rounded-full bg-success/10 text-success';
|
||||
|
||||
// Show endpoint count
|
||||
if (countEl && rotationConfig.providers) {
|
||||
var totalEndpoints = 0;
|
||||
rotationConfig.providers.forEach(function(p) {
|
||||
if (p.enabled) totalEndpoints += (p.useAllKeys ? 4 : 1); // Estimate
|
||||
});
|
||||
countEl.textContent = '~' + totalEndpoints + ' ' + t('codexlens.totalEndpoints').toLowerCase();
|
||||
}
|
||||
} else {
|
||||
badge.textContent = t('common.disabled');
|
||||
badge.className = 'text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground';
|
||||
if (countEl) countEl.textContent = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the rotation configuration modal
|
||||
*/
|
||||
async function showRotationConfigModal() {
|
||||
try {
|
||||
// Load current config if not already loaded
|
||||
if (!window.rotationConfig) {
|
||||
await loadRotationStatus();
|
||||
}
|
||||
|
||||
var rotationConfig = window.rotationConfig || {
|
||||
enabled: false,
|
||||
strategy: 'round_robin',
|
||||
defaultCooldown: 60,
|
||||
targetModel: 'qwen3-embedding',
|
||||
providers: []
|
||||
};
|
||||
var availableProviders = window.availableRotationProviders || [];
|
||||
|
||||
var modalHtml = buildRotationConfigModal(rotationConfig, availableProviders);
|
||||
|
||||
var tempContainer = document.createElement('div');
|
||||
tempContainer.innerHTML = modalHtml;
|
||||
var modal = tempContainer.firstElementChild;
|
||||
document.body.appendChild(modal);
|
||||
|
||||
if (window.lucide) lucide.createIcons();
|
||||
initRotationConfigEvents(rotationConfig, availableProviders);
|
||||
} catch (err) {
|
||||
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the rotation configuration modal HTML
|
||||
*/
|
||||
function buildRotationConfigModal(rotationConfig, availableProviders) {
|
||||
var isEnabled = rotationConfig.enabled || false;
|
||||
var strategy = rotationConfig.strategy || 'round_robin';
|
||||
var cooldown = rotationConfig.defaultCooldown || 60;
|
||||
var targetModel = rotationConfig.targetModel || 'qwen3-embedding';
|
||||
var configuredProviders = rotationConfig.providers || [];
|
||||
|
||||
// Build provider list HTML
|
||||
var providerListHtml = '';
|
||||
if (availableProviders.length === 0) {
|
||||
providerListHtml = '<div class="text-sm text-muted-foreground py-4 text-center">' + t('codexlens.noRotationProviders') + '</div>';
|
||||
} else {
|
||||
availableProviders.forEach(function(provider, index) {
|
||||
// Find if this provider is already configured
|
||||
var configured = configuredProviders.find(function(p) { return p.providerId === provider.providerId; });
|
||||
var isProviderEnabled = configured ? configured.enabled : false;
|
||||
var weight = configured ? configured.weight : 1;
|
||||
var maxConcurrent = configured ? configured.maxConcurrentPerKey : 4;
|
||||
var useAllKeys = configured ? configured.useAllKeys : true;
|
||||
|
||||
// Get model options
|
||||
var modelOptions = provider.embeddingModels.map(function(m) {
|
||||
var selected = configured && configured.modelId === m.modelId ? 'selected' : '';
|
||||
return '<option value="' + m.modelId + '" ' + selected + '>' + m.modelName + ' (' + m.dimensions + 'd)</option>';
|
||||
}).join('');
|
||||
|
||||
// Get key count
|
||||
var keyCount = provider.apiKeys.filter(function(k) { return k.enabled; }).length;
|
||||
|
||||
providerListHtml +=
|
||||
'<div class="border border-border rounded-lg p-3 ' + (isProviderEnabled ? 'bg-success/5 border-success/30' : 'bg-muted/30') + '" data-provider-id="' + provider.providerId + '">' +
|
||||
'<div class="flex items-center justify-between mb-2">' +
|
||||
'<div class="flex items-center gap-2">' +
|
||||
'<input type="checkbox" id="rotationProvider_' + index + '" ' + (isProviderEnabled ? 'checked' : '') +
|
||||
' class="rotation-provider-toggle" data-provider-id="' + provider.providerId + '" />' +
|
||||
'<label for="rotationProvider_' + index + '" class="font-medium text-sm">' + provider.providerName + '</label>' +
|
||||
'<span class="text-xs px-1.5 py-0.5 bg-muted rounded text-muted-foreground">' + keyCount + ' keys</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="grid grid-cols-2 gap-2 text-xs">' +
|
||||
'<div>' +
|
||||
'<label class="text-muted-foreground">Model</label>' +
|
||||
'<select class="w-full px-2 py-1 border border-border rounded bg-background text-sm rotation-model-select" data-provider-id="' + provider.providerId + '">' +
|
||||
modelOptions +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<label class="text-muted-foreground">' + t('codexlens.providerWeight') + '</label>' +
|
||||
'<input type="number" min="0.1" max="10" step="0.1" value="' + weight + '" ' +
|
||||
'class="w-full px-2 py-1 border border-border rounded bg-background text-sm rotation-weight-input" data-provider-id="' + provider.providerId + '" />' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<label class="text-muted-foreground">' + t('codexlens.maxConcurrentPerKey') + '</label>' +
|
||||
'<input type="number" min="1" max="16" value="' + maxConcurrent + '" ' +
|
||||
'class="w-full px-2 py-1 border border-border rounded bg-background text-sm rotation-concurrent-input" data-provider-id="' + provider.providerId + '" />' +
|
||||
'</div>' +
|
||||
'<div class="flex items-center gap-1">' +
|
||||
'<input type="checkbox" id="useAllKeys_' + index + '" ' + (useAllKeys ? 'checked' : '') +
|
||||
' class="rotation-use-all-keys" data-provider-id="' + provider.providerId + '" />' +
|
||||
'<label for="useAllKeys_' + index + '" class="text-muted-foreground">' + t('codexlens.useAllKeys') + '</label>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
});
|
||||
}
|
||||
|
||||
return '<div class="modal-backdrop" id="rotationConfigModal">' +
|
||||
'<div class="modal-container max-w-2xl">' +
|
||||
'<div class="modal-header">' +
|
||||
'<div class="flex items-center gap-3">' +
|
||||
'<div class="modal-icon">' +
|
||||
'<i data-lucide="rotate-cw" class="w-5 h-5"></i>' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<h2 class="text-lg font-bold">' + t('codexlens.rotation') + '</h2>' +
|
||||
'<p class="text-xs text-muted-foreground">' + t('codexlens.rotationDesc') + '</p>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<button onclick="closeRotationModal()" class="text-muted-foreground hover:text-foreground">' +
|
||||
'<i data-lucide="x" class="w-5 h-5"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'<div class="modal-body space-y-4">' +
|
||||
// Enable toggle
|
||||
'<div class="flex items-center justify-between p-3 bg-muted/30 rounded-lg">' +
|
||||
'<div class="flex items-center gap-2">' +
|
||||
'<i data-lucide="power" class="w-4 h-4 text-primary"></i>' +
|
||||
'<span class="font-medium">' + t('codexlens.rotationEnabled') + '</span>' +
|
||||
'</div>' +
|
||||
'<label class="relative inline-flex items-center cursor-pointer">' +
|
||||
'<input type="checkbox" id="rotationEnabledToggle" ' + (isEnabled ? 'checked' : '') + ' class="sr-only peer" />' +
|
||||
'<div class="w-11 h-6 bg-muted peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[\'\'] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary"></div>' +
|
||||
'</label>' +
|
||||
'</div>' +
|
||||
// Strategy and settings
|
||||
'<div class="grid grid-cols-2 gap-4">' +
|
||||
'<div>' +
|
||||
'<label class="block text-sm font-medium mb-1.5">' + t('codexlens.rotationStrategy') + '</label>' +
|
||||
'<select id="rotationStrategy" class="w-full px-3 py-2 border border-border rounded-lg bg-background text-sm">' +
|
||||
'<option value="round_robin" ' + (strategy === 'round_robin' ? 'selected' : '') + '>' + t('codexlens.strategyRoundRobin') + '</option>' +
|
||||
'<option value="latency_aware" ' + (strategy === 'latency_aware' ? 'selected' : '') + '>' + t('codexlens.strategyLatencyAware') + '</option>' +
|
||||
'<option value="weighted_random" ' + (strategy === 'weighted_random' ? 'selected' : '') + '>' + t('codexlens.strategyWeightedRandom') + '</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<label class="block text-sm font-medium mb-1.5">' + t('codexlens.cooldownSeconds') + '</label>' +
|
||||
'<input type="number" id="rotationCooldown" min="1" max="300" value="' + cooldown + '" ' +
|
||||
'class="w-full px-3 py-2 border border-border rounded-lg bg-background text-sm" />' +
|
||||
'<p class="text-xs text-muted-foreground mt-1">' + t('codexlens.cooldownHint') + '</p>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
// Target model
|
||||
'<div>' +
|
||||
'<label class="block text-sm font-medium mb-1.5">' + t('codexlens.targetModel') + '</label>' +
|
||||
'<input type="text" id="rotationTargetModel" value="' + targetModel + '" ' +
|
||||
'class="w-full px-3 py-2 border border-border rounded-lg bg-background text-sm" placeholder="qwen3-embedding" />' +
|
||||
'<p class="text-xs text-muted-foreground mt-1">' + t('codexlens.targetModelHint') + '</p>' +
|
||||
'</div>' +
|
||||
// Provider list
|
||||
'<div>' +
|
||||
'<label class="block text-sm font-medium mb-1.5">' + t('codexlens.rotationProviders') + '</label>' +
|
||||
'<div class="space-y-2 max-h-64 overflow-y-auto" id="rotationProviderList">' +
|
||||
providerListHtml +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="modal-footer">' +
|
||||
'<button onclick="closeRotationModal()" class="btn btn-outline">' + t('common.cancel') + '</button>' +
|
||||
'<button onclick="saveRotationConfig()" class="btn btn-primary">' +
|
||||
'<i data-lucide="save" class="w-4 h-4"></i> ' + t('common.save') +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize rotation config modal events
|
||||
*/
|
||||
function initRotationConfigEvents(rotationConfig, availableProviders) {
|
||||
// Store in window for save function
|
||||
window._rotationAvailableProviders = availableProviders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the rotation config modal
|
||||
*/
|
||||
function closeRotationModal() {
|
||||
var modal = document.getElementById('rotationConfigModal');
|
||||
if (modal) modal.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the rotation configuration
|
||||
*/
|
||||
async function saveRotationConfig() {
|
||||
try {
|
||||
var enabledToggle = document.getElementById('rotationEnabledToggle');
|
||||
var strategySelect = document.getElementById('rotationStrategy');
|
||||
var cooldownInput = document.getElementById('rotationCooldown');
|
||||
var targetModelInput = document.getElementById('rotationTargetModel');
|
||||
|
||||
var enabled = enabledToggle ? enabledToggle.checked : false;
|
||||
var strategy = strategySelect ? strategySelect.value : 'round_robin';
|
||||
var cooldown = cooldownInput ? parseInt(cooldownInput.value, 10) : 60;
|
||||
var targetModel = targetModelInput ? targetModelInput.value.trim() : 'qwen3-embedding';
|
||||
|
||||
// Collect provider configurations
|
||||
var providers = [];
|
||||
var providerToggles = document.querySelectorAll('.rotation-provider-toggle');
|
||||
providerToggles.forEach(function(toggle) {
|
||||
var providerId = toggle.getAttribute('data-provider-id');
|
||||
var isEnabled = toggle.checked;
|
||||
|
||||
var modelSelect = document.querySelector('.rotation-model-select[data-provider-id="' + providerId + '"]');
|
||||
var weightInput = document.querySelector('.rotation-weight-input[data-provider-id="' + providerId + '"]');
|
||||
var concurrentInput = document.querySelector('.rotation-concurrent-input[data-provider-id="' + providerId + '"]');
|
||||
var useAllKeysToggle = document.querySelector('.rotation-use-all-keys[data-provider-id="' + providerId + '"]');
|
||||
|
||||
providers.push({
|
||||
providerId: providerId,
|
||||
modelId: modelSelect ? modelSelect.value : '',
|
||||
weight: weightInput ? parseFloat(weightInput.value) || 1 : 1,
|
||||
maxConcurrentPerKey: concurrentInput ? parseInt(concurrentInput.value, 10) || 4 : 4,
|
||||
useAllKeys: useAllKeysToggle ? useAllKeysToggle.checked : true,
|
||||
enabled: isEnabled
|
||||
});
|
||||
});
|
||||
|
||||
var rotationConfig = {
|
||||
enabled: enabled,
|
||||
strategy: strategy,
|
||||
defaultCooldown: cooldown,
|
||||
targetModel: targetModel,
|
||||
providers: providers
|
||||
};
|
||||
|
||||
var response = await fetch('/api/litellm-api/codexlens/rotation', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(rotationConfig)
|
||||
});
|
||||
|
||||
var result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showRefreshToast(t('codexlens.rotationSaved'), 'success');
|
||||
window.rotationConfig = rotationConfig;
|
||||
updateRotationStatusDisplay(rotationConfig);
|
||||
closeRotationModal();
|
||||
} else {
|
||||
showRefreshToast(t('common.saveFailed') + ': ' + result.error, 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,6 +299,54 @@ export interface GlobalCacheSettings {
|
||||
maxTotalSizeMB: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* CodexLens embedding provider selection for rotation
|
||||
* Aggregates provider + model + all API keys
|
||||
*/
|
||||
export interface CodexLensEmbeddingProvider {
|
||||
/** Reference to provider credential ID */
|
||||
providerId: string;
|
||||
|
||||
/** Embedding model ID from the provider */
|
||||
modelId: string;
|
||||
|
||||
/** Whether to use all API keys from this provider (default: true) */
|
||||
useAllKeys: boolean;
|
||||
|
||||
/** Specific API key IDs to use (if useAllKeys is false) */
|
||||
selectedKeyIds?: string[];
|
||||
|
||||
/** Weight for weighted routing (default: 1.0, applies to all keys from this provider) */
|
||||
weight: number;
|
||||
|
||||
/** Maximum concurrent requests per key (default: 4) */
|
||||
maxConcurrentPerKey: number;
|
||||
|
||||
/** Whether this provider is enabled for rotation */
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* CodexLens multi-provider embedding rotation configuration
|
||||
* Aggregates multiple providers with same model for parallel rotation
|
||||
*/
|
||||
export interface CodexLensEmbeddingRotation {
|
||||
/** Whether multi-provider rotation is enabled */
|
||||
enabled: boolean;
|
||||
|
||||
/** Selection strategy: round_robin, latency_aware, weighted_random */
|
||||
strategy: 'round_robin' | 'latency_aware' | 'weighted_random';
|
||||
|
||||
/** Default cooldown seconds for rate-limited endpoints (default: 60) */
|
||||
defaultCooldown: number;
|
||||
|
||||
/** Target model name that all providers should support (e.g., "qwen3-embedding") */
|
||||
targetModel: string;
|
||||
|
||||
/** List of providers to aggregate for rotation */
|
||||
providers: CodexLensEmbeddingProvider[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete LiteLLM API configuration
|
||||
* Root configuration object stored in JSON file
|
||||
@@ -318,4 +366,7 @@ export interface LiteLLMApiConfig {
|
||||
|
||||
/** Global cache settings */
|
||||
globalCacheSettings: GlobalCacheSettings;
|
||||
|
||||
/** CodexLens multi-provider embedding rotation config */
|
||||
codexlensEmbeddingRotation?: CodexLensEmbeddingRotation;
|
||||
}
|
||||
|
||||
1
ccw/tsconfig.tsbuildinfo
Normal file
1
ccw/tsconfig.tsbuildinfo
Normal file
@@ -0,0 +1 @@
|
||||
{"root":["./src/cli.ts","./src/index.ts","./src/commands/cli.ts","./src/commands/core-memory.ts","./src/commands/hook.ts","./src/commands/install.ts","./src/commands/list.ts","./src/commands/memory.ts","./src/commands/serve.ts","./src/commands/session-path-resolver.ts","./src/commands/session.ts","./src/commands/stop.ts","./src/commands/tool.ts","./src/commands/uninstall.ts","./src/commands/upgrade.ts","./src/commands/view.ts","./src/config/litellm-api-config-manager.ts","./src/config/provider-models.ts","./src/config/storage-paths.ts","./src/core/cache-manager.ts","./src/core/claude-freshness.ts","./src/core/core-memory-store.ts","./src/core/dashboard-generator-patch.ts","./src/core/dashboard-generator.ts","./src/core/data-aggregator.ts","./src/core/history-importer.ts","./src/core/lite-scanner-complete.ts","./src/core/lite-scanner.ts","./src/core/manifest.ts","./src/core/memory-embedder-bridge.ts","./src/core/memory-store.ts","./src/core/server.ts","./src/core/session-clustering-service.ts","./src/core/session-scanner.ts","./src/core/websocket.ts","./src/core/routes/ccw-routes.ts","./src/core/routes/claude-routes.ts","./src/core/routes/cli-routes.ts","./src/core/routes/codexlens-routes.ts","./src/core/routes/core-memory-routes.ts","./src/core/routes/files-routes.ts","./src/core/routes/graph-routes.ts","./src/core/routes/help-routes.ts","./src/core/routes/hooks-routes.ts","./src/core/routes/litellm-api-routes.ts","./src/core/routes/litellm-routes.ts","./src/core/routes/mcp-routes.ts","./src/core/routes/mcp-templates-db.ts","./src/core/routes/memory-routes.ts","./src/core/routes/rules-routes.ts","./src/core/routes/session-routes.ts","./src/core/routes/skills-routes.ts","./src/core/routes/status-routes.ts","./src/core/routes/system-routes.ts","./src/mcp-server/index.ts","./src/tools/classify-folders.ts","./src/tools/claude-cli-tools.ts","./src/tools/cli-config-manager.ts","./src/tools/cli-executor.ts","./src/tools/cli-history-store.ts","./src/tools/codex-lens.ts","./src/tools/context-cache-store.ts","./src/tools/context-cache.ts","./src/tools/convert-tokens-to-css.ts","./src/tools/core-memory.ts","./src/tools/detect-changed-modules.ts","./src/tools/discover-design-files.ts","./src/tools/edit-file.ts","./src/tools/generate-module-docs.ts","./src/tools/get-modules-by-depth.ts","./src/tools/index.ts","./src/tools/litellm-client.ts","./src/tools/litellm-executor.ts","./src/tools/native-session-discovery.ts","./src/tools/notifier.ts","./src/tools/pattern-parser.ts","./src/tools/read-file.ts","./src/tools/resume-strategy.ts","./src/tools/session-content-parser.ts","./src/tools/session-manager.ts","./src/tools/smart-context.ts","./src/tools/smart-search.ts","./src/tools/storage-manager.ts","./src/tools/ui-generate-preview.js","./src/tools/ui-instantiate-prototypes.js","./src/tools/update-module-claude.js","./src/tools/write-file.ts","./src/types/config.ts","./src/types/index.ts","./src/types/litellm-api-config.ts","./src/types/session.ts","./src/types/tool.ts","./src/utils/browser-launcher.ts","./src/utils/file-utils.ts","./src/utils/path-resolver.ts","./src/utils/path-validator.ts","./src/utils/ui.ts"],"version":"5.9.3"}
|
||||
@@ -103,12 +103,12 @@ def init(
|
||||
"-l",
|
||||
help="Limit indexing to specific languages (repeat or comma-separated).",
|
||||
),
|
||||
workers: Optional[int] = typer.Option(None, "--workers", "-w", min=1, max=16, help="Parallel worker processes (default: auto-detect based on CPU count, max 16)."),
|
||||
workers: Optional[int] = typer.Option(None, "--workers", "-w", min=1, max=32, help="Parallel worker processes (default: auto-detect based on CPU count, max 32)."),
|
||||
force: bool = typer.Option(False, "--force", "-f", help="Force full reindex (skip incremental mode)."),
|
||||
no_embeddings: bool = typer.Option(False, "--no-embeddings", help="Skip automatic embedding generation (if semantic deps installed)."),
|
||||
embedding_backend: str = typer.Option("fastembed", "--embedding-backend", help="Embedding backend: fastembed (local) or litellm (remote API)."),
|
||||
embedding_model: str = typer.Option("code", "--embedding-model", help="Embedding model: profile name for fastembed (fast/code/multilingual/balanced) or model name for litellm (e.g. text-embedding-3-small)."),
|
||||
max_workers: int = typer.Option(1, "--max-workers", min=1, max=16, help="Max concurrent API calls for embedding generation. Recommended: 4-8 for litellm backend."),
|
||||
max_workers: int = typer.Option(1, "--max-workers", min=1, max=32, help="Max concurrent API calls for embedding generation. Recommended: 4-8 for litellm backend."),
|
||||
json_mode: bool = typer.Option(False, "--json", help="Output JSON response."),
|
||||
verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable debug logging."),
|
||||
) -> None:
|
||||
@@ -351,7 +351,7 @@ def search(
|
||||
Use 'codexlens embeddings-generate' to create embeddings first.
|
||||
|
||||
Hybrid Mode:
|
||||
Default weights: exact=0.4, fuzzy=0.3, vector=0.3
|
||||
Default weights: exact=0.3, fuzzy=0.1, vector=0.6
|
||||
Use --weights to customize (e.g., --weights 0.5,0.3,0.2)
|
||||
|
||||
Examples:
|
||||
@@ -1852,7 +1852,7 @@ def embeddings_generate(
|
||||
"--max-workers",
|
||||
"-w",
|
||||
min=1,
|
||||
max=16,
|
||||
max=32,
|
||||
help="Max concurrent API calls. Recommended: 4-8 for litellm backend. Default: 1 (sequential).",
|
||||
),
|
||||
json_mode: bool = typer.Option(False, "--json", help="Output JSON response."),
|
||||
|
||||
@@ -331,7 +331,7 @@ def generate_embeddings(
|
||||
if max_workers is None:
|
||||
if embedding_backend == "litellm":
|
||||
if endpoint_count > 1:
|
||||
max_workers = min(endpoint_count * 2, 16) # Cap at 16 workers
|
||||
max_workers = min(endpoint_count * 2, 32) # Cap at 32 workers
|
||||
else:
|
||||
max_workers = 4
|
||||
else:
|
||||
@@ -806,7 +806,7 @@ def generate_embeddings_recursive(
|
||||
if max_workers is None:
|
||||
if embedding_backend == "litellm":
|
||||
if endpoint_count > 1:
|
||||
max_workers = min(endpoint_count * 2, 16)
|
||||
max_workers = min(endpoint_count * 2, 32)
|
||||
else:
|
||||
max_workers = 4
|
||||
else:
|
||||
|
||||
@@ -27,11 +27,11 @@ class HybridSearchEngine:
|
||||
default_weights: Default RRF weights for each source
|
||||
"""
|
||||
|
||||
# Default RRF weights (exact: 40%, fuzzy: 30%, vector: 30%)
|
||||
# Default RRF weights (vector: 60%, exact: 30%, fuzzy: 10%)
|
||||
DEFAULT_WEIGHTS = {
|
||||
"exact": 0.4,
|
||||
"fuzzy": 0.3,
|
||||
"vector": 0.3,
|
||||
"exact": 0.3,
|
||||
"fuzzy": 0.1,
|
||||
"vector": 0.6,
|
||||
}
|
||||
|
||||
def __init__(self, weights: Optional[Dict[str, float]] = None):
|
||||
|
||||
@@ -25,7 +25,7 @@ def reciprocal_rank_fusion(
|
||||
results_map: Dictionary mapping source name to list of SearchResult objects
|
||||
Sources: 'exact', 'fuzzy', 'vector'
|
||||
weights: Dictionary mapping source name to weight (default: equal weights)
|
||||
Example: {'exact': 0.4, 'fuzzy': 0.3, 'vector': 0.3}
|
||||
Example: {'exact': 0.3, 'fuzzy': 0.1, 'vector': 0.6}
|
||||
k: Constant to avoid division by zero and control rank influence (default 60)
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -45,9 +45,9 @@ class TestHybridSearchBasics:
|
||||
"""Test HybridSearchEngine initializes with default weights."""
|
||||
engine = HybridSearchEngine()
|
||||
assert engine.weights == HybridSearchEngine.DEFAULT_WEIGHTS
|
||||
assert engine.weights["exact"] == 0.4
|
||||
assert engine.weights["fuzzy"] == 0.3
|
||||
assert engine.weights["vector"] == 0.3
|
||||
assert engine.weights["exact"] == 0.3
|
||||
assert engine.weights["fuzzy"] == 0.1
|
||||
assert engine.weights["vector"] == 0.6
|
||||
|
||||
def test_engine_custom_weights(self):
|
||||
"""Test HybridSearchEngine accepts custom weights."""
|
||||
|
||||
@@ -230,16 +230,16 @@ class TestRRFSyntheticRankings:
|
||||
vector = [SearchResult(path="c.py", score=8.0, excerpt="...")]
|
||||
|
||||
results_map = {"exact": exact, "fuzzy": fuzzy, "vector": vector}
|
||||
weights = {"exact": 0.4, "fuzzy": 0.3, "vector": 0.3}
|
||||
weights = {"exact": 0.3, "fuzzy": 0.1, "vector": 0.6}
|
||||
|
||||
fused = reciprocal_rank_fusion(results_map, weights=weights)
|
||||
|
||||
assert len(fused) == 3
|
||||
# Each appears in one source only, so scores differ by weights
|
||||
# a.py: 0.4/61 ≈ 0.0066
|
||||
# b.py: 0.3/61 ≈ 0.0049
|
||||
# c.py: 0.3/61 ≈ 0.0049
|
||||
assert fused[0].path == "a.py", "Exact (higher weight) should rank first"
|
||||
# c.py: 0.6/61 ≈ 0.0098 (vector, highest weight)
|
||||
# a.py: 0.3/61 ≈ 0.0049 (exact)
|
||||
# b.py: 0.1/61 ≈ 0.0016 (fuzzy)
|
||||
assert fused[0].path == "c.py", "Vector (higher weight) should rank first"
|
||||
|
||||
|
||||
class TestNormalizeBM25Score:
|
||||
|
||||
Reference in New Issue
Block a user