mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-10 02:24:35 +08:00
feat: Unified Embedding Pool with auto-discovery
Architecture refactoring for multi-provider rotation: Backend: - Add EmbeddingPoolConfig type with autoDiscover support - Implement discoverProvidersForModel() for auto-aggregation - Add GET/PUT /api/litellm-api/embedding-pool endpoints - Add GET /api/litellm-api/embedding-pool/discover/:model preview - Convert ccw-litellm status check to async with 5-min cache - Maintain backward compatibility with legacy rotation config Frontend: - Add "Embedding Pool" tab in API Settings - Auto-discover providers when target model selected - Show provider/key count with include/exclude controls - Increase sidebar width (280px → 320px) - Add sync result feedback on save Other: - Remove worker count limits (was max=32) - Add i18n translations (EN/CN) - Update .gitignore for .mcp.json 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,8 @@
|
||||
* Manages provider credentials, custom endpoints, and cache settings
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { StoragePaths, GlobalPaths, ensureStorageDir } from './storage-paths.js';
|
||||
import type {
|
||||
@@ -15,6 +16,7 @@ import type {
|
||||
CacheStrategy,
|
||||
CodexLensEmbeddingRotation,
|
||||
CodexLensEmbeddingProvider,
|
||||
EmbeddingPoolConfig,
|
||||
} from '../types/litellm-api-config.js';
|
||||
|
||||
/**
|
||||
@@ -372,11 +374,12 @@ export function getCodexLensEmbeddingRotation(baseDir: string): CodexLensEmbeddi
|
||||
|
||||
/**
|
||||
* Update CodexLens embedding rotation config
|
||||
* Also triggers sync to CodexLens settings.json
|
||||
*/
|
||||
export function updateCodexLensEmbeddingRotation(
|
||||
baseDir: string,
|
||||
rotationConfig: CodexLensEmbeddingRotation | undefined
|
||||
): void {
|
||||
): { syncResult: { success: boolean; message: string; endpointCount?: number } } {
|
||||
const config = loadLiteLLMApiConfig(baseDir);
|
||||
|
||||
if (rotationConfig) {
|
||||
@@ -386,6 +389,10 @@ export function updateCodexLensEmbeddingRotation(
|
||||
}
|
||||
|
||||
saveConfig(baseDir, config);
|
||||
|
||||
// Auto-sync to CodexLens settings.json
|
||||
const syncResult = syncCodexLensConfig(baseDir);
|
||||
return { syncResult };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -474,6 +481,7 @@ export function getEmbeddingProvidersForRotation(baseDir: string): Array<{
|
||||
/**
|
||||
* Generate rotation endpoints for ccw_litellm
|
||||
* Creates endpoint list from rotation config for parallel embedding
|
||||
* Supports both legacy codexlensEmbeddingRotation and new embeddingPoolConfig
|
||||
*/
|
||||
export function generateRotationEndpoints(baseDir: string): Array<{
|
||||
name: string;
|
||||
@@ -484,12 +492,115 @@ export function generateRotationEndpoints(baseDir: string): Array<{
|
||||
max_concurrent: number;
|
||||
}> {
|
||||
const config = loadLiteLLMApiConfig(baseDir);
|
||||
|
||||
// Prefer embeddingPoolConfig, fallback to codexlensEmbeddingRotation for backward compatibility
|
||||
const poolConfig = config.embeddingPoolConfig;
|
||||
const rotationConfig = config.codexlensEmbeddingRotation;
|
||||
|
||||
if (!rotationConfig || !rotationConfig.enabled) {
|
||||
return [];
|
||||
// Check if new poolConfig is enabled
|
||||
if (poolConfig && poolConfig.enabled) {
|
||||
return generateEndpointsFromPool(baseDir, poolConfig, config);
|
||||
}
|
||||
|
||||
// Fallback to legacy rotation config
|
||||
if (rotationConfig && rotationConfig.enabled) {
|
||||
return generateEndpointsFromLegacyRotation(baseDir, rotationConfig, config);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate endpoints from new embeddingPoolConfig (with auto-discovery support)
|
||||
*/
|
||||
function generateEndpointsFromPool(
|
||||
baseDir: string,
|
||||
poolConfig: EmbeddingPoolConfig,
|
||||
config: LiteLLMApiConfig
|
||||
): Array<{
|
||||
name: string;
|
||||
api_key: string;
|
||||
api_base: string;
|
||||
model: string;
|
||||
weight: number;
|
||||
max_concurrent: number;
|
||||
}> {
|
||||
const endpoints: Array<{
|
||||
name: string;
|
||||
api_key: string;
|
||||
api_base: string;
|
||||
model: string;
|
||||
weight: number;
|
||||
max_concurrent: number;
|
||||
}> = [];
|
||||
|
||||
if (poolConfig.autoDiscover) {
|
||||
// Auto-discover all providers offering targetModel
|
||||
const discovered = discoverProvidersForModel(baseDir, poolConfig.targetModel);
|
||||
const excludedIds = new Set(poolConfig.excludedProviderIds || []);
|
||||
|
||||
for (const disc of discovered) {
|
||||
// Skip excluded providers
|
||||
if (excludedIds.has(disc.providerId)) continue;
|
||||
|
||||
// Find the provider config
|
||||
const provider = config.providers.find(p => p.id === disc.providerId);
|
||||
if (!provider || !provider.enabled) continue;
|
||||
|
||||
// Find the embedding model
|
||||
const embeddingModel = provider.embeddingModels?.find(m => m.id === disc.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) {
|
||||
// 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 (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: 1.0, // Default weight for auto-discovered providers
|
||||
max_concurrent: poolConfig.defaultMaxConcurrentPerKey,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate endpoints from legacy codexlensEmbeddingRotation config
|
||||
*/
|
||||
function generateEndpointsFromLegacyRotation(
|
||||
baseDir: string,
|
||||
rotationConfig: CodexLensEmbeddingRotation,
|
||||
config: LiteLLMApiConfig
|
||||
): Array<{
|
||||
name: string;
|
||||
api_key: string;
|
||||
api_base: string;
|
||||
model: string;
|
||||
weight: number;
|
||||
max_concurrent: number;
|
||||
}> {
|
||||
const endpoints: Array<{
|
||||
name: string;
|
||||
api_key: string;
|
||||
@@ -551,6 +662,191 @@ export function generateRotationEndpoints(baseDir: string): Array<{
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync CodexLens settings with CCW API config
|
||||
* Writes rotation endpoints to ~/.codexlens/settings.json
|
||||
* This enables the Python backend to use UI-configured rotation
|
||||
* Supports both new embeddingPoolConfig and legacy codexlensEmbeddingRotation
|
||||
*/
|
||||
export function syncCodexLensConfig(baseDir: string): { success: boolean; message: string; endpointCount?: number } {
|
||||
try {
|
||||
const config = loadLiteLLMApiConfig(baseDir);
|
||||
|
||||
// Prefer embeddingPoolConfig, fallback to codexlensEmbeddingRotation
|
||||
const poolConfig = config.embeddingPoolConfig;
|
||||
const rotationConfig = config.codexlensEmbeddingRotation;
|
||||
|
||||
// Get CodexLens settings path
|
||||
const codexlensDir = join(homedir(), '.codexlens');
|
||||
const settingsPath = join(codexlensDir, 'settings.json');
|
||||
|
||||
// Ensure directory exists
|
||||
if (!existsSync(codexlensDir)) {
|
||||
mkdirSync(codexlensDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Load existing settings or create new
|
||||
let settings: Record<string, unknown> = {};
|
||||
if (existsSync(settingsPath)) {
|
||||
try {
|
||||
settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
||||
} catch {
|
||||
settings = {};
|
||||
}
|
||||
}
|
||||
|
||||
// Check if either config is enabled
|
||||
const isPoolEnabled = poolConfig && poolConfig.enabled;
|
||||
const isRotationEnabled = rotationConfig && rotationConfig.enabled;
|
||||
|
||||
// If neither is enabled, remove rotation endpoints and return
|
||||
if (!isPoolEnabled && !isRotationEnabled) {
|
||||
if (settings.litellm_rotation_endpoints) {
|
||||
delete settings.litellm_rotation_endpoints;
|
||||
delete settings.litellm_rotation_strategy;
|
||||
delete settings.litellm_rotation_cooldown;
|
||||
delete settings.litellm_target_model;
|
||||
writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
||||
}
|
||||
return { success: true, message: 'Rotation disabled, cleared endpoints', endpointCount: 0 };
|
||||
}
|
||||
|
||||
// Generate rotation endpoints (function handles priority internally)
|
||||
const endpoints = generateRotationEndpoints(baseDir);
|
||||
|
||||
if (endpoints.length === 0) {
|
||||
return { success: false, message: 'No valid endpoints generated from rotation config' };
|
||||
}
|
||||
|
||||
// Update settings with rotation config (use poolConfig if available)
|
||||
settings.litellm_rotation_endpoints = endpoints;
|
||||
|
||||
if (isPoolEnabled) {
|
||||
settings.litellm_rotation_strategy = poolConfig!.strategy;
|
||||
settings.litellm_rotation_cooldown = poolConfig!.defaultCooldown;
|
||||
settings.litellm_target_model = poolConfig!.targetModel;
|
||||
} else {
|
||||
settings.litellm_rotation_strategy = rotationConfig!.strategy;
|
||||
settings.litellm_rotation_cooldown = rotationConfig!.defaultCooldown;
|
||||
settings.litellm_target_model = rotationConfig!.targetModel;
|
||||
}
|
||||
|
||||
// Write updated settings
|
||||
writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Synced ${endpoints.length} rotation endpoints to CodexLens`,
|
||||
endpointCount: endpoints.length,
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error('[LiteLLM Config] Failed to sync CodexLens config:', errorMessage);
|
||||
return { success: false, message: `Sync failed: ${errorMessage}` };
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// Embedding Pool Management (Generic, with Auto-Discovery)
|
||||
// ===========================
|
||||
|
||||
/**
|
||||
* Get embedding pool config
|
||||
*/
|
||||
export function getEmbeddingPoolConfig(baseDir: string): EmbeddingPoolConfig | undefined {
|
||||
const config = loadLiteLLMApiConfig(baseDir);
|
||||
return config.embeddingPoolConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update embedding pool config
|
||||
* Also triggers sync to CodexLens settings.json if enabled
|
||||
*/
|
||||
export function updateEmbeddingPoolConfig(
|
||||
baseDir: string,
|
||||
poolConfig: EmbeddingPoolConfig | undefined
|
||||
): { syncResult: { success: boolean; message: string; endpointCount?: number } } {
|
||||
const config = loadLiteLLMApiConfig(baseDir);
|
||||
|
||||
if (poolConfig) {
|
||||
config.embeddingPoolConfig = poolConfig;
|
||||
} else {
|
||||
delete config.embeddingPoolConfig;
|
||||
}
|
||||
|
||||
saveConfig(baseDir, config);
|
||||
|
||||
// Auto-sync to CodexLens settings.json
|
||||
const syncResult = syncCodexLensConfig(baseDir);
|
||||
return { syncResult };
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover all providers that offer a specific embedding model
|
||||
* Returns list of {providerId, providerName, modelId, modelName, apiKeys[]}
|
||||
*/
|
||||
export function discoverProvidersForModel(baseDir: string, targetModel: string): Array<{
|
||||
providerId: string;
|
||||
providerName: string;
|
||||
modelId: string;
|
||||
modelName: string;
|
||||
apiKeys: Array<{ keyId: string; keyLabel: string; enabled: boolean }>;
|
||||
}> {
|
||||
const config = loadLiteLLMApiConfig(baseDir);
|
||||
const result: Array<{
|
||||
providerId: string;
|
||||
providerName: string;
|
||||
modelId: string;
|
||||
modelName: string;
|
||||
apiKeys: Array<{ keyId: string; keyLabel: string; enabled: boolean }>;
|
||||
}> = [];
|
||||
|
||||
for (const provider of config.providers) {
|
||||
if (!provider.enabled) continue;
|
||||
|
||||
// Check if provider has embedding models matching targetModel
|
||||
const matchingModels = (provider.embeddingModels || []).filter(
|
||||
m => m.enabled && (m.id === targetModel || m.name === targetModel)
|
||||
);
|
||||
|
||||
if (matchingModels.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,
|
||||
});
|
||||
}
|
||||
|
||||
// Add each matching model
|
||||
for (const model of matchingModels) {
|
||||
result.push({
|
||||
providerId: provider.id,
|
||||
providerName: provider.name,
|
||||
modelId: model.id,
|
||||
modelName: model.name,
|
||||
apiKeys,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// YAML Config Generation for ccw_litellm
|
||||
// ===========================
|
||||
@@ -713,4 +1009,4 @@ function objectToYaml(obj: unknown, indent: number = 0): string {
|
||||
}
|
||||
|
||||
// Re-export types
|
||||
export type { ProviderCredential, CustomEndpoint, ProviderType, CacheStrategy, CodexLensEmbeddingRotation, CodexLensEmbeddingProvider };
|
||||
export type { ProviderCredential, CustomEndpoint, ProviderType, CacheStrategy, CodexLensEmbeddingRotation, CodexLensEmbeddingProvider, EmbeddingPoolConfig };
|
||||
|
||||
Reference in New Issue
Block a user