feat: Enhance CLI tools and settings management

- Added auto-initialization of CSRF token for state-changing requests in cli-manager.js.
- Refactored Claude CLI Tools configuration to separate tools and settings into cli-tools.json and cli-settings.json respectively.
- Introduced new interfaces for Claude CLI Tools and Settings, including support for tags and primary models.
- Implemented loading and saving functions for CLI settings, ensuring backward compatibility with legacy combined config.
- Updated functions to synchronize tags between CLI tools and configuration manager.
- Added error handling and logging for loading and saving configurations.
- Created initial cli-settings.json with default settings.
This commit is contained in:
catlog22
2026-01-08 22:00:07 +08:00
parent 0bd2cff5b7
commit ea5c0bc9a4
12 changed files with 1376 additions and 180 deletions

18
.claude/cli-settings.json Normal file
View File

@@ -0,0 +1,18 @@
{
"version": "1.0.0",
"defaultTool": "gemini",
"promptFormat": "plain",
"smartContext": {
"enabled": false,
"maxFiles": 10
},
"nativeResume": true,
"recursiveQuery": true,
"cache": {
"injectionMode": "auto",
"defaultPrefix": "",
"defaultSuffix": ""
},
"codeIndexMcp": "ace",
"$schema": "./cli-settings.schema.json"
}

View File

@@ -1,39 +1,50 @@
{
"version": "1.0.0",
"version": "2.0.0",
"tools": {
"gemini": {
"enabled": true,
"isBuiltin": true,
"command": "gemini",
"description": "Google AI for code analysis"
"description": "Google AI for code analysis",
"tags": []
},
"qwen": {
"enabled": true,
"isBuiltin": true,
"command": "qwen",
"description": "Alibaba AI assistant"
"description": "Alibaba AI assistant",
"tags": []
},
"codex": {
"enabled": true,
"isBuiltin": true,
"command": "codex",
"description": "OpenAI code generation"
"description": "OpenAI code generation",
"tags": []
},
"claude": {
"enabled": true,
"isBuiltin": true,
"command": "claude",
"description": "Anthropic AI assistant"
"description": "Anthropic AI assistant",
"tags": []
},
"opencode": {
"enabled": true,
"isBuiltin": true,
"command": "opencode",
"description": "OpenCode AI assistant",
"primaryModel": "opencode/glm-4.7-free"
"primaryModel": "opencode/glm-4.7-free",
"tags": []
}
},
"customEndpoints": [],
"customEndpoints": [
{
"id": "g25",
"name": "g25",
"enabled": true
}
],
"defaultTool": "gemini",
"settings": {
"promptFormat": "plain",
@@ -48,7 +59,7 @@
"defaultPrefix": "",
"defaultSuffix": ""
},
"codeIndexMcp": "ace"
"codeIndexMcp": "codexlens"
},
"$schema": "./cli-tools.schema.json"
}

View File

@@ -1033,5 +1033,219 @@ function objectToYaml(obj: unknown, indent: number = 0): string {
return String(obj);
}
// ===========================
// Multi-Model Pool Management
// ===========================
/**
* Migrate legacy embeddingPoolConfig to new modelPools array
* This function ensures backward compatibility with existing configurations
*/
function migrateEmbeddingPoolToModelPools(config: LiteLLMApiConfig): void {
// Skip if already has modelPools or no legacy config
if (config.modelPools && config.modelPools.length > 0) return;
if (!config.embeddingPoolConfig) return;
// Convert legacy embeddingPoolConfig to ModelPoolConfig
const legacyPool = config.embeddingPoolConfig;
const modelPool: import('../types/litellm-api-config.js').ModelPoolConfig = {
id: `pool-embedding-${Date.now()}`,
modelType: 'embedding',
enabled: legacyPool.enabled,
targetModel: legacyPool.targetModel,
strategy: legacyPool.strategy,
autoDiscover: legacyPool.autoDiscover,
excludedProviderIds: legacyPool.excludedProviderIds || [],
defaultCooldown: legacyPool.defaultCooldown,
defaultMaxConcurrentPerKey: legacyPool.defaultMaxConcurrentPerKey,
name: `Embedding Pool - ${legacyPool.targetModel}`,
description: 'Migrated from legacy embeddingPoolConfig',
};
config.modelPools = [modelPool];
// Keep legacy config for backward compatibility with old CodexLens versions
}
/**
* Get all model pool configurations
* Returns empty array if no pools configured
*/
export function getModelPools(baseDir: string): import('../types/litellm-api-config.js').ModelPoolConfig[] {
const config = loadLiteLLMApiConfig(baseDir);
// Auto-migrate if needed
migrateEmbeddingPoolToModelPools(config);
return config.modelPools || [];
}
/**
* Get a specific model pool by ID
*/
export function getModelPool(
baseDir: string,
poolId: string
): import('../types/litellm-api-config.js').ModelPoolConfig | undefined {
const pools = getModelPools(baseDir);
return pools.find(p => p.id === poolId);
}
/**
* Add a new model pool configuration
*/
export function addModelPool(
baseDir: string,
poolConfig: Omit<import('../types/litellm-api-config.js').ModelPoolConfig, 'id'>
): { poolId: string; syncResult?: { success: boolean; message: string; endpointCount?: number } } {
const config = loadLiteLLMApiConfig(baseDir);
// Auto-migrate if needed
migrateEmbeddingPoolToModelPools(config);
// Ensure modelPools array exists
if (!config.modelPools) {
config.modelPools = [];
}
// Generate unique ID
const poolId = `pool-${poolConfig.modelType}-${Date.now()}`;
const newPool: import('../types/litellm-api-config.js').ModelPoolConfig = {
...poolConfig,
id: poolId,
};
config.modelPools.push(newPool);
saveConfig(baseDir, config);
// Sync to CodexLens if this is an embedding pool
const syncResult = poolConfig.modelType === 'embedding' && poolConfig.enabled
? syncCodexLensConfig(baseDir)
: undefined;
return { poolId, syncResult };
}
/**
* Update an existing model pool configuration
*/
export function updateModelPool(
baseDir: string,
poolId: string,
updates: Partial<Omit<import('../types/litellm-api-config.js').ModelPoolConfig, 'id'>>
): { success: boolean; syncResult?: { success: boolean; message: string; endpointCount?: number } } {
const config = loadLiteLLMApiConfig(baseDir);
// Auto-migrate if needed
migrateEmbeddingPoolToModelPools(config);
if (!config.modelPools) {
return { success: false };
}
const poolIndex = config.modelPools.findIndex(p => p.id === poolId);
if (poolIndex === -1) {
return { success: false };
}
// Apply updates
config.modelPools[poolIndex] = {
...config.modelPools[poolIndex],
...updates,
};
saveConfig(baseDir, config);
// Sync to CodexLens if this is an enabled embedding pool
const pool = config.modelPools[poolIndex];
const syncResult = pool.modelType === 'embedding' && pool.enabled
? syncCodexLensConfig(baseDir)
: undefined;
return { success: true, syncResult };
}
/**
* Delete a model pool configuration
*/
export function deleteModelPool(
baseDir: string,
poolId: string
): { success: boolean; syncResult?: { success: boolean; message: string; endpointCount?: number } } {
const config = loadLiteLLMApiConfig(baseDir);
if (!config.modelPools) {
return { success: false };
}
const poolIndex = config.modelPools.findIndex(p => p.id === poolId);
if (poolIndex === -1) {
return { success: false };
}
const deletedPool = config.modelPools[poolIndex];
config.modelPools.splice(poolIndex, 1);
saveConfig(baseDir, config);
// Sync to CodexLens if we deleted an embedding pool
const syncResult = deletedPool.modelType === 'embedding'
? syncCodexLensConfig(baseDir)
: undefined;
return { success: true, syncResult };
}
/**
* Get available models for a specific model type
* Used for pool configuration UI
*/
export function getAvailableModelsForType(
baseDir: string,
modelType: import('../types/litellm-api-config.js').ModelPoolType
): Array<{ modelId: string; modelName: string; providers: string[] }> {
const config = loadLiteLLMApiConfig(baseDir);
const availableModels: Array<{ modelId: string; modelName: string; providers: string[] }> = [];
const modelMap = new Map<string, { modelId: string; modelName: string; providers: string[] }>();
for (const provider of config.providers) {
if (!provider.enabled) continue;
let models: typeof provider.embeddingModels | undefined;
switch (modelType) {
case 'embedding':
models = provider.embeddingModels;
break;
case 'llm':
models = provider.llmModels;
break;
case 'reranker':
models = provider.rerankerModels;
break;
}
if (!models) continue;
for (const model of models) {
if (!model.enabled) continue;
const key = model.id;
if (modelMap.has(key)) {
modelMap.get(key)!.providers.push(provider.name);
} else {
modelMap.set(key, {
modelId: model.id,
modelName: model.name,
providers: [provider.name],
});
}
}
}
availableModels.push(...Array.from(modelMap.values()));
return availableModels;
}
// Re-export types
export type { ProviderCredential, CustomEndpoint, ProviderType, CacheStrategy, CodexLensEmbeddingRotation, CodexLensEmbeddingProvider, EmbeddingPoolConfig };

View File

@@ -34,6 +34,8 @@ import {
import {
loadClaudeCliTools,
saveClaudeCliTools,
loadClaudeCliSettings,
saveClaudeCliSettings,
updateClaudeToolEnabled,
updateClaudeCacheSettings,
getClaudeCliToolsInfo,
@@ -704,11 +706,13 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
// API: Get CLI Tools Config from .claude/cli-tools.json (with fallback to global)
if (pathname === '/api/cli/tools-config' && req.method === 'GET') {
try {
const config = loadClaudeCliTools(initialPath);
const toolsConfig = loadClaudeCliTools(initialPath);
const settingsConfig = loadClaudeCliSettings(initialPath);
const info = getClaudeCliToolsInfo(initialPath);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
...config,
tools: toolsConfig,
settings: settingsConfig,
_configInfo: info
}));
} catch (err) {
@@ -722,32 +726,55 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
if (pathname === '/api/cli/tools-config' && req.method === 'PUT') {
handlePostRequest(req, res, async (body: unknown) => {
try {
const updates = body as Partial<any>;
const config = loadClaudeCliTools(initialPath);
const updates = body as { tools?: any; settings?: any };
// Merge updates
const updatedConfig = {
...config,
...updates,
tools: { ...config.tools, ...(updates.tools || {}) },
settings: {
...config.settings,
...(updates.settings || {}),
cache: {
...config.settings.cache,
...(updates.settings?.cache || {})
// Update tools config if provided
if (updates.tools) {
const currentTools = loadClaudeCliTools(initialPath);
const updatedTools = {
...currentTools,
tools: { ...currentTools.tools, ...(updates.tools.tools || {}) },
customEndpoints: updates.tools.customEndpoints || currentTools.customEndpoints
};
saveClaudeCliTools(initialPath, updatedTools);
}
// Update settings config if provided
if (updates.settings) {
const currentSettings = loadClaudeCliSettings(initialPath);
const s = updates.settings;
// Deep merge: only update fields that are explicitly provided
const updatedSettings = {
...currentSettings,
// Scalar fields: only update if explicitly provided
...(s.defaultTool !== undefined && { defaultTool: s.defaultTool }),
...(s.promptFormat !== undefined && { promptFormat: s.promptFormat }),
...(s.nativeResume !== undefined && { nativeResume: s.nativeResume }),
...(s.recursiveQuery !== undefined && { recursiveQuery: s.recursiveQuery }),
...(s.codeIndexMcp !== undefined && { codeIndexMcp: s.codeIndexMcp }),
// Nested objects: deep merge
smartContext: {
...currentSettings.smartContext,
...(s.smartContext || {})
},
cache: {
...currentSettings.cache,
...(s.cache || {})
}
};
saveClaudeCliSettings(initialPath, updatedSettings);
}
saveClaudeCliTools(initialPath, updatedConfig);
const toolsConfig = loadClaudeCliTools(initialPath);
const settingsConfig = loadClaudeCliSettings(initialPath);
broadcastToClients({
type: 'CLI_TOOLS_CONFIG_UPDATED',
payload: { config: updatedConfig, timestamp: new Date().toISOString() }
payload: { tools: toolsConfig, settings: settingsConfig, timestamp: new Date().toISOString() }
});
return { success: true, config: updatedConfig };
return { success: true, tools: toolsConfig, settings: settingsConfig };
} catch (err) {
return { error: (err as Error).message, status: 500 };
}
@@ -782,14 +809,14 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
handlePostRequest(req, res, async (body: unknown) => {
try {
const cacheSettings = body as { injectionMode?: string; defaultPrefix?: string; defaultSuffix?: string };
const config = updateClaudeCacheSettings(initialPath, cacheSettings as any);
const settings = updateClaudeCacheSettings(initialPath, cacheSettings as any);
broadcastToClients({
type: 'CLI_CACHE_SETTINGS_UPDATED',
payload: { cache: config.settings.cache, timestamp: new Date().toISOString() }
payload: { cache: settings.cache, timestamp: new Date().toISOString() }
});
return { success: true, config };
return { success: true, settings };
} catch (err) {
return { error: (err as Error).message, status: 500 };
}

View File

@@ -4,9 +4,35 @@
*/
import { fileURLToPath } from 'url';
import { dirname, join as pathJoin } from 'path';
import { z } from 'zod';
import { getSystemPython } from '../../utils/python-utils.js';
import type { RouteContext } from './types.js';
// ========== Input Validation Schemas ==========
/**
* Validation schema for ModelPoolConfig
* Used to validate incoming API requests for model pool operations
*/
const ModelPoolConfigSchema = z.object({
modelType: z.enum(['embedding', 'llm', 'reranker']),
enabled: z.boolean(),
targetModel: z.string().min(1, 'Target model is required'),
strategy: z.enum(['round_robin', 'latency_aware', 'weighted_random']),
autoDiscover: z.boolean(),
excludedProviderIds: z.array(z.string()).optional().default([]),
defaultCooldown: z.number().int().min(0).default(60),
defaultMaxConcurrentPerKey: z.number().int().min(1).default(4),
name: z.string().optional(),
description: z.string().optional(),
});
/**
* Partial schema for updating ModelPoolConfig
* All fields are optional for PATCH-like updates
*/
const ModelPoolConfigUpdateSchema = ModelPoolConfigSchema.partial();
// Get current module path for package-relative lookups
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -39,6 +65,12 @@ import {
getEmbeddingPoolConfig,
updateEmbeddingPoolConfig,
discoverProvidersForModel,
getModelPools,
getModelPool,
addModelPool,
updateModelPool,
deleteModelPool,
getAvailableModelsForType,
type ProviderCredential,
type CustomEndpoint,
type ProviderType,
@@ -856,6 +888,186 @@ export async function handleLiteLLMApiRoutes(ctx: RouteContext): Promise<boolean
return true;
}
// ========== Multi-Model Pool Management ==========
// GET /api/litellm-api/model-pools - Get all model pool configurations
if (pathname === '/api/litellm-api/model-pools' && req.method === 'GET') {
try {
const pools = getModelPools(initialPath);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ pools }));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (err as Error).message }));
}
return true;
}
// GET /api/litellm-api/model-pools/:id - Get specific pool configuration
const poolGetMatch = pathname.match(/^\/api\/litellm-api\/model-pools\/([^/]+)$/);
if (poolGetMatch && req.method === 'GET') {
const poolId = decodeURIComponent(poolGetMatch[1]);
try {
const pool = getModelPool(initialPath, poolId);
if (!pool) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Pool not found' }));
return true;
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ pool }));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (err as Error).message }));
}
return true;
}
// POST /api/litellm-api/model-pools - Create new model pool
if (pathname === '/api/litellm-api/model-pools' && req.method === 'POST') {
handlePostRequest(req, res, async (body: unknown) => {
// Validate input using zod schema
const validationResult = ModelPoolConfigSchema.safeParse(body);
if (!validationResult.success) {
return {
error: 'Invalid request body',
details: validationResult.error.issues.map(e => ({
field: String(e.path.join('.')),
message: e.message
})),
status: 400
};
}
try {
const poolConfig = validationResult.data;
const result = addModelPool(initialPath, poolConfig);
broadcastToClients({
type: 'MODEL_POOL_CREATED',
payload: { poolId: result.poolId, timestamp: new Date().toISOString() }
});
return { success: true, ...result };
} catch (err) {
return { error: (err as Error).message, status: 500 };
}
});
return true;
}
// PUT /api/litellm-api/model-pools/:id - Update model pool
const poolPutMatch = pathname.match(/^\/api\/litellm-api\/model-pools\/([^/]+)$/);
if (poolPutMatch && req.method === 'PUT') {
const poolId = decodeURIComponent(poolPutMatch[1]);
handlePostRequest(req, res, async (body: unknown) => {
// Validate input using partial schema (all fields optional for updates)
const validationResult = ModelPoolConfigUpdateSchema.safeParse(body);
if (!validationResult.success) {
return {
error: 'Invalid request body',
details: validationResult.error.issues.map(e => ({
field: String(e.path.join('.')),
message: e.message
})),
status: 400
};
}
try {
const updates = validationResult.data;
const result = updateModelPool(initialPath, poolId, updates);
if (!result.success) {
return { error: 'Pool not found', status: 404 };
}
broadcastToClients({
type: 'MODEL_POOL_UPDATED',
payload: { poolId, syncResult: result.syncResult, timestamp: new Date().toISOString() }
});
return result;
} catch (err) {
return { error: (err as Error).message, status: 500 };
}
});
return true;
}
// DELETE /api/litellm-api/model-pools/:id - Delete model pool
const poolDeleteMatch = pathname.match(/^\/api\/litellm-api\/model-pools\/([^/]+)$/);
if (poolDeleteMatch && req.method === 'DELETE') {
const poolId = decodeURIComponent(poolDeleteMatch[1]);
try {
const result = deleteModelPool(initialPath, poolId);
if (!result.success) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Pool not found' }));
return true;
}
broadcastToClients({
type: 'MODEL_POOL_DELETED',
payload: { poolId, syncResult: result.syncResult, timestamp: new Date().toISOString() }
});
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(result));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (err as Error).message }));
}
return true;
}
// GET /api/litellm-api/model-pools/available-models/:modelType - Get available models for type
const availableModelsMatch = pathname.match(/^\/api\/litellm-api\/model-pools\/available-models\/([^/]+)$/);
if (availableModelsMatch && req.method === 'GET') {
const modelType = decodeURIComponent(availableModelsMatch[1]) as import('../../types/litellm-api-config.js').ModelPoolType;
try {
const availableModels = getAvailableModelsForType(initialPath, modelType);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ availableModels, modelType }));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (err as Error).message }));
}
return true;
}
// GET /api/litellm-api/model-pools/discover/:modelType/:model - Discover providers for model
const discoverPoolMatch = pathname.match(/^\/api\/litellm-api\/model-pools\/discover\/([^/]+)\/([^/]+)$/);
if (discoverPoolMatch && req.method === 'GET') {
const modelType = decodeURIComponent(discoverPoolMatch[1]);
const targetModel = decodeURIComponent(discoverPoolMatch[2]);
try {
const discovered = discoverProvidersForModel(initialPath, targetModel);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
modelType,
targetModel,
discovered,
count: discovered.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 () => {

View File

@@ -550,7 +550,7 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
// Serve dashboard HTML
if (pathname === '/' || pathname === '/index.html') {
if (isLocalhostRequest(req)) {
// Set session cookie and CSRF token for all requests
const tokenResult = tokenManager.getOrCreateAuthToken();
setAuthCookie(res, tokenResult.token, tokenResult.expiresAt);
@@ -558,7 +558,7 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
const csrfToken = getCsrfTokenManager().generateToken(sessionId);
res.setHeader('X-CSRF-Token', csrfToken);
setCsrfCookie(res, csrfToken, 15 * 60);
}
const html = generateServerDashboard(initialPath);
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(html);

View File

@@ -972,27 +972,31 @@ select.cli-input {
.sidebar-tabs {
display: flex;
gap: 0;
flex-wrap: nowrap;
gap: 0.125rem;
padding: 0.5rem;
background: hsl(var(--muted) / 0.3);
border-bottom: 1px solid hsl(var(--border));
}
.sidebar-tab {
flex: 1;
flex: 1 1 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.25rem;
gap: 0.125rem;
padding: 0.5rem 0.25rem;
font-size: 0.75rem;
font-weight: 500;
color: hsl(var(--muted-foreground));
background: transparent;
border: none;
border-radius: 0.375rem;
border: 1px solid transparent;
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
min-width: 0;
}
.sidebar-tab:hover {
@@ -1002,12 +1006,14 @@ select.cli-input {
.sidebar-tab.active {
color: hsl(var(--primary));
background: hsl(var(--primary) / 0.1);
background: hsl(var(--card));
border-color: hsl(var(--border));
box-shadow: 0 1px 3px hsl(var(--foreground) / 0.05);
}
.sidebar-tab i {
width: 14px;
height: 14px;
width: 18px;
height: 18px;
}
/* Sidebar content areas */
@@ -1107,13 +1113,13 @@ select.cli-input {
/* Responsive adjustments for tabs */
@media (max-width: 768px) {
.sidebar-tab {
padding: 0.5rem 0.5rem;
font-size: 0.7rem;
padding: 0.5rem 0.625rem;
font-size: 0.75rem;
}
.sidebar-tab i {
width: 12px;
height: 12px;
width: 16px;
height: 16px;
}
}

View File

@@ -11,13 +11,19 @@ let selectedProviderId = null;
let providerSearchQuery = '';
let activeModelTab = 'llm';
let expandedModelGroups = new Set();
let activeSidebarTab = 'providers'; // 'providers' | 'endpoints' | 'cache' | 'embedding-pool' | 'cli-settings'
let activeSidebarTab = 'providers'; // 'providers' | 'endpoints' | 'cache' | 'embedding-pool' | 'model-pools' | 'cli-settings'
// Embedding Pool state
// Embedding Pool state (legacy, kept for backward compatibility)
let embeddingPoolConfig = null;
let embeddingPoolAvailableModels = [];
let embeddingPoolDiscoveredProviders = [];
// Multi-Model Pool state
let modelPools = [];
let selectedPoolId = null;
let poolAvailableModels = {};
let poolDiscoveredProviders = {};
// CLI Settings state
let cliSettingsData = null;
let selectedCliSettingsId = null;
@@ -30,6 +36,9 @@ const CCW_LITELLM_STATUS_CACHE_TTL = 60000; // 60 seconds
// Track if this is the first render (force refresh on first load)
let isFirstApiSettingsRender = true;
// Note: CSRF token management (csrfToken, initCsrfToken, csrfFetch) is defined in cli-manager.js
// and shared across all views when files are bundled together
// ========== Data Loading ==========
/**
@@ -130,6 +139,57 @@ async function loadCliSettings(forceRefresh = false) {
}
}
/**
* Load all model pool configurations
*/
async function loadModelPools() {
try {
const response = await fetch('/api/litellm-api/model-pools');
if (!response.ok) throw new Error('Failed to load model pools');
const data = await response.json();
modelPools = data.pools || [];
return modelPools;
} catch (err) {
console.error('Failed to load model pools:', err);
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
return [];
}
}
/**
* Load available models for a specific model type
*/
async function loadAvailableModelsForType(modelType) {
try {
const response = await fetch('/api/litellm-api/model-pools/available-models/' + modelType);
if (!response.ok) throw new Error('Failed to load available models');
const data = await response.json();
poolAvailableModels[modelType] = data.availableModels || [];
return data.availableModels;
} catch (err) {
console.error('Failed to load available models:', err);
return [];
}
}
/**
* Discover providers for a specific model in pool context
*/
async function discoverProvidersForPool(modelType, targetModel) {
try {
const response = await fetch('/api/litellm-api/model-pools/discover/' + modelType + '/' + encodeURIComponent(targetModel));
if (!response.ok) throw new Error('Failed to discover providers');
const data = await response.json();
const key = modelType + ':' + targetModel;
poolDiscoveredProviders[key] = data.discovered || [];
return data;
} catch (err) {
console.error('Failed to discover providers:', err);
poolDiscoveredProviders[key] = [];
return null;
}
}
/**
* Save CLI Settings endpoint
*/
@@ -138,7 +198,8 @@ async function saveCliSettingsEndpoint(data) {
const method = data.id ? 'PUT' : 'POST';
const url = data.id ? '/api/cli/settings/' + data.id : '/api/cli/settings';
const response = await fetch(url, {
await initCsrfToken();
const response = await csrfFetch(url, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
@@ -174,7 +235,8 @@ async function deleteCliSettingsEndpoint(endpointId) {
if (!confirm(t('apiSettings.confirmDeleteSettings'))) return;
try {
const response = await fetch('/api/cli/settings/' + endpointId, {
await initCsrfToken();
const response = await csrfFetch('/api/cli/settings/' + endpointId, {
method: 'DELETE'
});
@@ -237,7 +299,8 @@ async function saveEmbeddingPoolConfig() {
defaultMaxConcurrentPerKey: defaultMaxConcurrentPerKey
} : null;
const response = await fetch('/api/litellm-api/embedding-pool', {
await initCsrfToken();
const response = await csrfFetch('/api/litellm-api/embedding-pool', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(poolConfig)
@@ -260,9 +323,9 @@ async function saveEmbeddingPoolConfig() {
// Update sidebar summary
const sidebarContainer = document.querySelector('.api-settings-sidebar');
if (sidebarContainer) {
const contentArea = sidebarContainer.querySelector('.provider-list, .endpoints-list, .embedding-pool-sidebar-info, .embedding-pool-sidebar-summary, .cache-sidebar-info');
if (contentArea && contentArea.parentElement) {
contentArea.parentElement.innerHTML = renderEmbeddingPoolSidebar();
const contentArea = sidebarContainer.querySelector('.embedding-pool-sidebar-info, .embedding-pool-sidebar-summary');
if (contentArea) {
contentArea.outerHTML = renderEmbeddingPoolSidebar();
if (window.lucide) lucide.createIcons();
}
}
@@ -294,9 +357,9 @@ async function toggleProviderExclusion(providerId) {
renderDiscoveredProviders();
// Update sidebar summary
const sidebarContainer = document.querySelector('.api-settings-sidebar .embedding-pool-sidebar-summary');
if (sidebarContainer && sidebarContainer.parentElement) {
sidebarContainer.parentElement.innerHTML = renderEmbeddingPoolSidebar();
const sidebarContainer = document.querySelector('.api-settings-sidebar .embedding-pool-sidebar-summary, .api-settings-sidebar .embedding-pool-sidebar-info');
if (sidebarContainer) {
sidebarContainer.outerHTML = renderEmbeddingPoolSidebar();
if (window.lucide) lucide.createIcons();
}
}
@@ -557,7 +620,8 @@ async function saveProvider() {
: '/api/litellm-api/providers';
const method = providerId ? 'PUT' : 'POST';
const response = await fetch(url, {
await initCsrfToken();
const response = await csrfFetch(url, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(providerData)
@@ -585,7 +649,8 @@ async function deleteProvider(providerId) {
if (!confirm(t('apiSettings.confirmDeleteProvider'))) return;
try {
const response = await fetch('/api/litellm-api/providers/' + providerId, {
await initCsrfToken();
const response = await csrfFetch('/api/litellm-api/providers/' + providerId, {
method: 'DELETE'
});
@@ -624,7 +689,8 @@ async function testProviderConnection(providerIdParam) {
}
try {
const response = await fetch('/api/litellm-api/providers/' + providerId + '/test', {
await initCsrfToken();
const response = await csrfFetch('/api/litellm-api/providers/' + providerId + '/test', {
method: 'POST'
});
@@ -917,7 +983,8 @@ async function saveEndpoint() {
: '/api/litellm-api/endpoints';
const method = form.dataset.endpointId ? 'PUT' : 'POST';
const response = await fetch(url, {
await initCsrfToken();
const response = await csrfFetch(url, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(endpointData)
@@ -945,7 +1012,8 @@ async function deleteEndpoint(endpointId) {
if (!confirm(t('apiSettings.confirmDeleteEndpoint'))) return;
try {
const response = await fetch('/api/litellm-api/endpoints/' + endpointId, {
await initCsrfToken();
const response = await csrfFetch('/api/litellm-api/endpoints/' + endpointId, {
method: 'DELETE'
});
@@ -1018,7 +1086,8 @@ async function clearCache() {
if (!confirm(t('apiSettings.confirmClearCache'))) return;
try {
const response = await fetch('/api/litellm-api/cache/clear', {
await initCsrfToken();
const response = await csrfFetch('/api/litellm-api/cache/clear', {
method: 'POST'
});
@@ -1042,7 +1111,8 @@ async function toggleGlobalCache() {
const enabled = document.getElementById('global-cache-enabled').checked;
try {
const response = await fetch('/api/litellm-api/config/cache', {
await initCsrfToken();
const response = await csrfFetch('/api/litellm-api/config/cache', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: enabled })
@@ -1095,8 +1165,11 @@ async function renderApiSettings() {
'<button class="sidebar-tab' + (activeSidebarTab === 'cli-settings' ? ' active' : '') + '" onclick="switchSidebarTab(\'cli-settings\')">' +
'<i data-lucide="settings"></i> ' + t('apiSettings.cliSettings') +
'</button>' +
'<button class="sidebar-tab' + (activeSidebarTab === 'model-pools' ? ' active' : '') + '" onclick="switchSidebarTab(\'model-pools\')">' +
'<i data-lucide="layers"></i> ' + t('apiSettings.modelPools') +
'</button>' +
'<button class="sidebar-tab' + (activeSidebarTab === 'embedding-pool' ? ' active' : '') + '" onclick="switchSidebarTab(\'embedding-pool\')">' +
'<i data-lucide="repeat"></i> ' + t('apiSettings.embeddingPool') +
'<i data-lucide="repeat"></i> ' + t('apiSettings.embeddingPool') + ' (Legacy)' +
'</button>' +
'<button class="sidebar-tab' + (activeSidebarTab === 'cache' ? ' active' : '') + '" onclick="switchSidebarTab(\'cache\')">' +
'<i data-lucide="database"></i> ' + t('apiSettings.cache') +
@@ -1127,6 +1200,15 @@ async function renderApiSettings() {
await loadEmbeddingPoolConfig();
}
sidebarContentHtml = renderEmbeddingPoolSidebar();
} else if (activeSidebarTab === 'model-pools') {
// Load model pools first if not already loaded
if (!modelPools || modelPools.length === 0) {
await loadModelPools();
}
sidebarContentHtml = '<div class="model-pools-list" id="model-pools-list"></div>';
addButtonHtml = '<button class="btn btn-primary btn-full" onclick="showAddModelPoolModal()">' +
'<i data-lucide="plus"></i> ' + t('apiSettings.addModelPool') +
'</button>';
} else if (activeSidebarTab === 'cache') {
sidebarContentHtml = '<div class="cache-sidebar-info" style="padding: 1rem; color: var(--text-secondary); font-size: 0.875rem;">' +
'<p>' + t('apiSettings.cacheTabHint') + '</p>' +
@@ -1177,6 +1259,16 @@ async function renderApiSettings() {
renderEndpointsMainPanel();
} else if (activeSidebarTab === 'embedding-pool') {
renderEmbeddingPoolMainPanel();
} else if (activeSidebarTab === 'model-pools') {
renderModelPoolsList();
// Auto-select first pool if exists
if (!selectedPoolId && modelPools && modelPools.length > 0) {
selectModelPool(modelPools[0].id);
} else if (selectedPoolId) {
renderModelPoolDetail(selectedPoolId);
} else {
renderModelPoolEmptyState();
}
} else if (activeSidebarTab === 'cache') {
renderCacheMainPanel();
} else if (activeSidebarTab === 'cli-settings') {
@@ -1574,7 +1666,8 @@ function getDefaultApiBase(type) {
*/
async function toggleProviderEnabled(providerId, enabled) {
try {
var response = await fetch('/api/litellm-api/providers/' + providerId, {
await initCsrfToken();
var response = await csrfFetch('/api/litellm-api/providers/' + providerId, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: enabled })
@@ -2015,7 +2108,7 @@ function saveNewModel(event, providerId, modelType) {
}
models.push(newModel);
return fetch('/api/litellm-api/providers/' + providerId, {
return csrfFetch('/api/litellm-api/providers/' + providerId, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ [modelsKey]: models })
@@ -2260,7 +2353,7 @@ function saveModelSettings(event, providerId, modelId, modelType) {
var updateData = {};
updateData[modelsKey] = models;
return fetch('/api/litellm-api/providers/' + providerId, {
return csrfFetch('/api/litellm-api/providers/' + providerId, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updateData)
@@ -2299,7 +2392,7 @@ function deleteModel(providerId, modelId, modelType) {
var updateData = {};
updateData[modelsKey] = updatedModels;
return fetch('/api/litellm-api/providers/' + providerId, {
return csrfFetch('/api/litellm-api/providers/' + providerId, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updateData)
@@ -2342,7 +2435,8 @@ async function saveProviderApiBase(providerId) {
}
try {
var response = await fetch('/api/litellm-api/providers/' + providerId, {
await initCsrfToken();
var response = await csrfFetch('/api/litellm-api/providers/' + providerId, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ apiBase: newApiBase || undefined })
@@ -2394,7 +2488,8 @@ async function deleteProviderWithConfirm(providerId) {
if (!confirm(t('apiSettings.confirmDeleteProvider'))) return;
try {
var response = await fetch('/api/litellm-api/providers/' + providerId, {
await initCsrfToken();
var response = await csrfFetch('/api/litellm-api/providers/' + providerId, {
method: 'DELETE'
});
@@ -2428,7 +2523,8 @@ async function deleteProviderWithConfirm(providerId) {
*/
async function syncConfigToCodexLens() {
try {
var response = await fetch('/api/litellm-api/config/sync', {
await initCsrfToken();
var response = await csrfFetch('/api/litellm-api/config/sync', {
method: 'POST'
});
@@ -2992,9 +3088,10 @@ async function onTargetModelChange(modelId) {
// Update sidebar summary
const sidebarContainer = document.querySelector('.api-settings-sidebar');
if (sidebarContainer) {
const contentArea = sidebarContainer.querySelector('.provider-list, .endpoints-list, .embedding-pool-sidebar-info, .embedding-pool-sidebar-summary, .cache-sidebar-info');
if (contentArea && contentArea.parentElement) {
contentArea.parentElement.innerHTML = renderEmbeddingPoolSidebar();
const contentArea = sidebarContainer.querySelector('.provider-list, .endpoints-list, .embedding-pool-sidebar-info, .embedding-pool-sidebar-summary, .cache-sidebar-info, .cli-settings-list');
if (contentArea) {
// Use outerHTML to replace only the content area, not the entire sidebar
contentArea.outerHTML = renderEmbeddingPoolSidebar();
if (window.lucide) lucide.createIcons();
}
}
@@ -3253,7 +3350,7 @@ function addApiKey(providerId) {
.then(function(provider) {
const apiKeys = provider.apiKeys || [];
apiKeys.push(newKey);
return fetch('/api/litellm-api/providers/' + providerId, {
return csrfFetch('/api/litellm-api/providers/' + providerId, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ apiKeys: apiKeys })
@@ -3279,7 +3376,7 @@ function removeApiKey(providerId, keyId) {
.then(function(res) { return res.json(); })
.then(function(provider) {
const apiKeys = (provider.apiKeys || []).filter(function(k) { return k.id !== keyId; });
return fetch('/api/litellm-api/providers/' + providerId, {
return csrfFetch('/api/litellm-api/providers/' + providerId, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ apiKeys: apiKeys })
@@ -3307,7 +3404,7 @@ function updateApiKeyField(providerId, keyId, field, value) {
if (keyIndex >= 0) {
apiKeys[keyIndex][field] = value;
}
return fetch('/api/litellm-api/providers/' + providerId, {
return csrfFetch('/api/litellm-api/providers/' + providerId, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ apiKeys: apiKeys })
@@ -3322,7 +3419,7 @@ function updateApiKeyField(providerId, keyId, field, value) {
* Update provider routing strategy
*/
function updateProviderRouting(providerId, strategy) {
fetch('/api/litellm-api/providers/' + providerId, {
csrfFetch('/api/litellm-api/providers/' + providerId, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ routingStrategy: strategy })
@@ -3340,7 +3437,7 @@ function updateHealthCheckEnabled(providerId, enabled) {
.then(function(provider) {
const healthCheck = provider.healthCheck || { intervalSeconds: 300, cooldownSeconds: 5, failureThreshold: 3 };
healthCheck.enabled = enabled;
return fetch('/api/litellm-api/providers/' + providerId, {
return csrfFetch('/api/litellm-api/providers/' + providerId, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ healthCheck: healthCheck })
@@ -3365,7 +3462,7 @@ function updateHealthCheckField(providerId, field, value) {
.then(function(provider) {
const healthCheck = provider.healthCheck || { enabled: false, intervalSeconds: 300, cooldownSeconds: 5, failureThreshold: 3 };
healthCheck[field] = value;
return fetch('/api/litellm-api/providers/' + providerId, {
return csrfFetch('/api/litellm-api/providers/' + providerId, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ healthCheck: healthCheck })
@@ -3385,7 +3482,7 @@ function testApiKey(providerId, keyId) {
btn.classList.add('testing');
btn.textContent = t('apiSettings.testingKey');
fetch('/api/litellm-api/providers/' + providerId + '/test-key', {
csrfFetch('/api/litellm-api/providers/' + providerId + '/test-key', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ keyId: keyId })
@@ -3525,7 +3622,8 @@ async function installCcwLitellm() {
}
try {
var response = await fetch('/api/litellm-api/ccw-litellm/install', {
await initCsrfToken();
var response = await csrfFetch('/api/litellm-api/ccw-litellm/install', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
@@ -3566,7 +3664,8 @@ async function uninstallCcwLitellm() {
}
try {
var response = await fetch('/api/litellm-api/ccw-litellm/uninstall', {
await initCsrfToken();
var response = await csrfFetch('/api/litellm-api/ccw-litellm/uninstall', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
@@ -3953,6 +4052,387 @@ async function submitCliSettings() {
}
}
// ========== Multi-Model Pool Management ==========
/**
* Render model pools list in sidebar
*/
function renderModelPoolsList() {
var container = document.getElementById('model-pools-list');
if (!container) return;
if (!modelPools || modelPools.length === 0) {
container.innerHTML = '<div class="empty-state" style="padding: 2rem; text-align: center; color: var(--text-secondary);">' +
'<i data-lucide="layers" style="width: 48px; height: 48px; margin-bottom: 1rem;"></i>' +
'<p>' + t('apiSettings.noPoolsConfigured') + '</p>' +
'</div>';
if (window.lucide) lucide.createIcons();
return;
}
// Group pools by type
var poolsByType = {
embedding: [],
llm: [],
reranker: []
};
modelPools.forEach(function(pool) {
if (poolsByType[pool.modelType]) {
poolsByType[pool.modelType].push(pool);
}
});
var html = '';
// Render each type group
['embedding', 'llm', 'reranker'].forEach(function(type) {
var pools = poolsByType[type];
if (pools.length === 0) return;
var typeLabel = type === 'embedding' ? t('apiSettings.embeddingPools') :
type === 'llm' ? t('apiSettings.llmPools') :
t('apiSettings.rerankerPools');
html += '<div class="pool-type-group" style="margin-bottom: 1.5rem;">' +
'<div class="pool-type-header" style="padding: 0.5rem; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; color: var(--text-secondary); border-bottom: 1px solid var(--border);">' +
typeLabel +
'</div>';
pools.forEach(function(pool) {
var isSelected = selectedPoolId === pool.id;
var statusClass = pool.enabled ? 'status-enabled' : 'status-disabled';
var statusText = pool.enabled ? t('common.enabled') : t('common.disabled');
html += '<div class="pool-item' + (isSelected ? ' selected' : '') + '" onclick="selectModelPool(\'' + pool.id + '\')" style="padding: 0.75rem; cursor: pointer; border-bottom: 1px solid var(--border);">' +
'<div style="display: flex; justify-content: space-between; align-items: center;">' +
'<div style="flex: 1; min-width: 0;">' +
'<div style="font-weight: 500; margin-bottom: 0.25rem;">' + escapeHtml(pool.name || pool.targetModel) + '</div>' +
'<div style="font-size: 0.75rem; color: var(--text-secondary);">' + escapeHtml(pool.targetModel) + '</div>' +
'</div>' +
'<span class="status-badge ' + statusClass + '" style="font-size: 0.7rem; padding: 0.25rem 0.5rem; border-radius: 4px;">' + statusText + '</span>' +
'</div>' +
'</div>';
});
html += '</div>';
});
container.innerHTML = html;
if (window.lucide) lucide.createIcons();
}
/**
* Select a model pool
*/
function selectModelPool(poolId) {
selectedPoolId = poolId;
renderModelPoolsList();
renderModelPoolDetail(poolId);
}
/**
* Render model pool detail in main panel
*/
function renderModelPoolDetail(poolId) {
var container = document.getElementById('provider-detail-panel');
if (!container) return;
var pool = modelPools.find(function(p) { return p.id === poolId; });
if (!pool) {
renderModelPoolEmptyState();
return;
}
var typeLabel = pool.modelType === 'embedding' ? t('apiSettings.embedding') :
pool.modelType === 'llm' ? t('apiSettings.llm') :
t('apiSettings.reranker');
var html = '<div class="provider-detail">' +
'<div class="provider-detail-header">' +
'<div>' +
'<h2>' + escapeHtml(pool.name || pool.targetModel) + '</h2>' +
'<p style="color: var(--text-secondary); margin-top: 0.5rem;">' + typeLabel + ' Pool</p>' +
'</div>' +
'<div class="provider-actions">' +
'<button class="btn btn-secondary" onclick="editModelPool(\'' + pool.id + '\')"><i data-lucide="edit-2"></i> ' + t('common.edit') + '</button>' +
'<button class="btn btn-danger" onclick="deleteModelPool(\'' + pool.id + '\')"><i data-lucide="trash-2"></i> ' + t('common.delete') + '</button>' +
'</div>' +
'</div>' +
'<div class="provider-detail-body">' +
// Basic Info
'<div class="form-section">' +
'<h3>' + t('apiSettings.basicInfo') + '</h3>' +
'<div class="info-grid">' +
'<div class="info-item"><label>' + t('apiSettings.status') + '</label><span class="status-badge ' + (pool.enabled ? 'status-enabled' : 'status-disabled') + '">' + (pool.enabled ? t('common.enabled') : t('common.disabled')) + '</span></div>' +
'<div class="info-item"><label>' + t('apiSettings.modelType') + '</label><span>' + typeLabel + '</span></div>' +
'<div class="info-item"><label>' + t('apiSettings.targetModel') + '</label><span>' + escapeHtml(pool.targetModel) + '</span></div>' +
'<div class="info-item"><label>' + t('apiSettings.strategy') + '</label><span>' + pool.strategy + '</span></div>' +
'<div class="info-item"><label>' + t('apiSettings.autoDiscover') + '</label><span>' + (pool.autoDiscover ? t('common.yes') : t('common.no')) + '</span></div>' +
'<div class="info-item"><label>' + t('apiSettings.cooldown') + '</label><span>' + pool.defaultCooldown + 's</span></div>' +
'<div class="info-item"><label>' + t('apiSettings.maxConcurrent') + '</label><span>' + pool.defaultMaxConcurrentPerKey + '</span></div>' +
'</div>' +
'</div>';
if (pool.description) {
html += '<div class="form-section">' +
'<h3>' + t('apiSettings.description') + '</h3>' +
'<p>' + escapeHtml(pool.description) + '</p>' +
'</div>';
}
// Excluded Providers
if (pool.excludedProviderIds && pool.excludedProviderIds.length > 0) {
html += '<div class="form-section">' +
'<h3>' + t('apiSettings.excludedProviders') + '</h3>' +
'<div class="excluded-providers-list">';
pool.excludedProviderIds.forEach(function(providerId) {
html += '<span class="tag">' + escapeHtml(providerId) + '</span>';
});
html += '</div></div>';
}
html += '</div></div>';
container.innerHTML = html;
if (window.lucide) lucide.createIcons();
}
/**
* Render empty state for model pools
*/
function renderModelPoolEmptyState() {
var container = document.getElementById('provider-detail-panel');
if (!container) return;
container.innerHTML = '<div class="empty-state">' +
'<i data-lucide="layers" style="width: 64px; height: 64px; margin-bottom: 1rem;"></i>' +
'<h3>' + t('apiSettings.noPoolSelected') + '</h3>' +
'<p>' + t('apiSettings.selectPoolFromList') + '</p>' +
'</div>';
if (window.lucide) lucide.createIcons();
}
/**
* Show add model pool modal
*/
function showAddModelPoolModal() {
var modalHtml = '<div class="generic-modal-overlay active" id="add-pool-modal">' +
'<div class="generic-modal" style="max-width: 600px;">' +
'<div class="generic-modal-header">' +
'<h3 class="generic-modal-title">' + t('apiSettings.addModelPool') + '</h3>' +
'<button class="generic-modal-close" onclick="closeAddPoolModal()">&times;</button>' +
'</div>' +
'<div class="generic-modal-body">' +
'<form id="add-pool-form" class="api-settings-form" onsubmit="submitModelPool(event)">' +
'<div class="form-group">' +
'<label>' + t('apiSettings.modelType') + ' *</label>' +
'<select id="pool-model-type" class="cli-input" required onchange="onPoolModelTypeChange()">' +
'<option value="">Select Type</option>' +
'<option value="embedding">Embedding</option>' +
'<option value="llm">LLM</option>' +
'<option value="reranker">Reranker</option>' +
'</select>' +
'</div>' +
'<div class="form-group">' +
'<label>' + t('apiSettings.poolName') + '</label>' +
'<input type="text" id="pool-name" class="cli-input" placeholder="e.g., Primary Embedding Pool" />' +
'</div>' +
'<div class="form-group">' +
'<label>' + t('apiSettings.targetModel') + ' *</label>' +
'<select id="pool-target-model" class="cli-input" required disabled>' +
'<option value="">Select model type first</option>' +
'</select>' +
'</div>' +
'<div class="form-group">' +
'<label>' + t('apiSettings.strategy') + ' *</label>' +
'<select id="pool-strategy" class="cli-input" required>' +
'<option value="round_robin">Round Robin</option>' +
'<option value="latency_aware" selected>Latency Aware</option>' +
'<option value="weighted_random">Weighted Random</option>' +
'</select>' +
'</div>' +
'<div class="form-group">' +
'<label>' + t('apiSettings.cooldown') + ' (seconds)</label>' +
'<input type="number" id="pool-cooldown" class="cli-input" value="60" min="0" />' +
'</div>' +
'<div class="form-group">' +
'<label>' + t('apiSettings.maxConcurrent') + '</label>' +
'<input type="number" id="pool-max-concurrent" class="cli-input" value="4" min="1" />' +
'</div>' +
'<div class="form-group">' +
'<label>' + t('apiSettings.description') + '</label>' +
'<textarea id="pool-description" class="cli-input" rows="2" placeholder="Optional description"></textarea>' +
'</div>' +
'<div class="form-group">' +
'<label class="checkbox-label">' +
'<input type="checkbox" id="pool-enabled" checked /> ' + t('apiSettings.enablePool') +
'</label>' +
'</div>' +
'<div class="form-group">' +
'<label class="checkbox-label">' +
'<input type="checkbox" id="pool-auto-discover" checked /> ' + t('apiSettings.autoDiscoverProviders') +
'</label>' +
'</div>' +
'<div class="modal-actions">' +
'<button type="button" class="btn btn-secondary" onclick="closeAddPoolModal()"><i data-lucide="x"></i> ' + t('common.cancel') + '</button>' +
'<button type="submit" class="btn btn-primary"><i data-lucide="check"></i> ' + t('common.save') + '</button>' +
'</div>' +
'</form>' +
'</div>' +
'</div>' +
'</div>';
document.body.insertAdjacentHTML('beforeend', modalHtml);
if (window.lucide) lucide.createIcons();
}
/**
* Close add pool modal
*/
function closeAddPoolModal() {
var modal = document.getElementById('add-pool-modal');
if (modal) modal.remove();
}
/**
* Handle pool model type change
*/
async function onPoolModelTypeChange() {
var modelType = document.getElementById('pool-model-type').value;
var targetModelSelect = document.getElementById('pool-target-model');
if (!modelType) {
targetModelSelect.disabled = true;
targetModelSelect.innerHTML = '<option value="">Select model type first</option>';
return;
}
// Load available models for this type
var models = await loadAvailableModelsForType(modelType);
targetModelSelect.disabled = false;
targetModelSelect.innerHTML = '<option value="">Select a model</option>';
models.forEach(function(model) {
var option = document.createElement('option');
option.value = model.modelId;
option.textContent = model.modelName + ' (' + model.providers.length + ' providers)';
targetModelSelect.appendChild(option);
});
}
/**
* Submit model pool form
*/
async function submitModelPool(event) {
event.preventDefault();
var poolData = {
modelType: document.getElementById('pool-model-type').value,
name: document.getElementById('pool-name').value,
targetModel: document.getElementById('pool-target-model').value,
strategy: document.getElementById('pool-strategy').value,
defaultCooldown: parseInt(document.getElementById('pool-cooldown').value),
defaultMaxConcurrentPerKey: parseInt(document.getElementById('pool-max-concurrent').value),
description: document.getElementById('pool-description').value,
enabled: document.getElementById('pool-enabled').checked,
autoDiscover: document.getElementById('pool-auto-discover').checked,
excludedProviderIds: []
};
try {
await initCsrfToken();
var response = await csrfFetch('/api/litellm-api/model-pools', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(poolData)
});
if (!response.ok) {
var err = await response.json();
throw new Error(err.error || 'Failed to create pool');
}
var result = await response.json();
showRefreshToast(t('apiSettings.poolCreated'), 'success');
closeAddPoolModal();
// Reload pools and switch to model-pools tab
await loadModelPools();
activeSidebarTab = 'model-pools';
renderApiSettings();
} catch (err) {
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
}
}
/**
* Edit model pool
*/
function editModelPool(poolId) {
// TODO: Implement edit modal
showRefreshToast('Edit functionality coming soon', 'info');
}
/**
* Delete model pool
*/
async function deleteModelPool(poolId) {
if (!confirm(t('apiSettings.confirmDeletePool'))) {
return;
}
try {
await initCsrfToken();
var response = await csrfFetch('/api/litellm-api/model-pools/' + poolId, {
method: 'DELETE'
});
if (!response.ok) {
var err = await response.json();
throw new Error(err.error || 'Failed to delete pool');
}
showRefreshToast(t('apiSettings.poolDeleted'), 'success');
// Reload pools
selectedPoolId = null;
await loadModelPools();
renderApiSettings();
} catch (err) {
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
}
}
// Make model pool functions globally accessible
window.loadModelPools = loadModelPools;
window.renderModelPoolsList = renderModelPoolsList;
window.selectModelPool = selectModelPool;
window.renderModelPoolDetail = renderModelPoolDetail;
window.renderModelPoolEmptyState = renderModelPoolEmptyState;
window.showAddModelPoolModal = showAddModelPoolModal;
window.closeAddPoolModal = closeAddPoolModal;
window.onPoolModelTypeChange = onPoolModelTypeChange;
window.submitModelPool = submitModelPool;
window.editModelPool = editModelPool;
window.deleteModelPool = deleteModelPool;
// Make CLI Settings functions globally accessible
window.loadCliSettings = loadCliSettings;
window.saveCliSettingsEndpoint = saveCliSettingsEndpoint;

View File

@@ -22,6 +22,12 @@ async function csrfFetch(url, options) {
// Add CSRF token header for state-changing methods
var method = (options.method || 'GET').toUpperCase();
// Auto-initialize CSRF token for state-changing requests
if (['POST', 'PUT', 'PATCH', 'DELETE'].indexOf(method) !== -1) {
await initCsrfToken();
}
if (['POST', 'PUT', 'PATCH', 'DELETE'].indexOf(method) !== -1 && csrfToken) {
options.headers['X-CSRF-Token'] = csrfToken;
}

View File

@@ -1,8 +1,9 @@
/**
* Claude CLI Tools Configuration Manager
* Manages .claude/cli-tools.json with fallback:
* 1. Project workspace: {projectDir}/.claude/cli-tools.json (priority)
* 2. Global: ~/.claude/cli-tools.json (fallback)
* Manages .claude/cli-tools.json (tools) and .claude/cli-settings.json (settings)
* with fallback:
* 1. Project workspace: {projectDir}/.claude/ (priority)
* 2. Global: ~/.claude/ (fallback)
*/
import * as fs from 'fs';
import * as path from 'path';
@@ -15,6 +16,15 @@ export interface ClaudeCliTool {
isBuiltin: boolean;
command: string;
description: string;
primaryModel?: string;
tags: string[];
}
export interface ClaudeCustomEndpoint {
id: string;
name: string;
enabled: boolean;
tags: string[];
}
export interface ClaudeCacheSettings {
@@ -23,17 +33,19 @@ export interface ClaudeCacheSettings {
defaultSuffix: string;
}
// New: Tools-only config (cli-tools.json)
export interface ClaudeCliToolsConfig {
$schema?: string;
version: string;
tools: Record<string, ClaudeCliTool>;
customEndpoints: Array<{
id: string;
name: string;
enabled: boolean;
}>;
customEndpoints: ClaudeCustomEndpoint[];
}
// New: Settings-only config (cli-settings.json)
export interface ClaudeCliSettingsConfig {
$schema?: string;
version: string;
defaultTool: string;
settings: {
promptFormat: 'plain' | 'yaml' | 'json';
smartContext: {
enabled: boolean;
@@ -42,43 +54,73 @@ export interface ClaudeCliToolsConfig {
nativeResume: boolean;
recursiveQuery: boolean;
cache: ClaudeCacheSettings;
codeIndexMcp: 'codexlens' | 'ace' | 'none'; // Code Index MCP provider
codeIndexMcp: 'codexlens' | 'ace' | 'none';
}
// Legacy combined config (for backward compatibility)
export interface ClaudeCliCombinedConfig extends ClaudeCliToolsConfig {
defaultTool?: string;
settings?: {
promptFormat?: 'plain' | 'yaml' | 'json';
smartContext?: {
enabled?: boolean;
maxFiles?: number;
};
nativeResume?: boolean;
recursiveQuery?: boolean;
cache?: Partial<ClaudeCacheSettings>;
codeIndexMcp?: 'codexlens' | 'ace' | 'none';
};
}
// ========== Default Config ==========
const DEFAULT_CONFIG: ClaudeCliToolsConfig = {
version: '1.0.0',
const DEFAULT_TOOLS_CONFIG: ClaudeCliToolsConfig = {
version: '2.0.0',
tools: {
gemini: {
enabled: true,
isBuiltin: true,
command: 'gemini',
description: 'Google AI for code analysis'
description: 'Google AI for code analysis',
tags: []
},
qwen: {
enabled: true,
isBuiltin: true,
command: 'qwen',
description: 'Alibaba AI assistant'
description: 'Alibaba AI assistant',
tags: []
},
codex: {
enabled: true,
isBuiltin: true,
command: 'codex',
description: 'OpenAI code generation'
description: 'OpenAI code generation',
tags: []
},
claude: {
enabled: true,
isBuiltin: true,
command: 'claude',
description: 'Anthropic AI assistant'
description: 'Anthropic AI assistant',
tags: []
},
opencode: {
enabled: true,
isBuiltin: true,
command: 'opencode',
description: 'OpenCode AI assistant',
primaryModel: 'opencode/glm-4.7-free',
tags: []
}
},
customEndpoints: [],
customEndpoints: []
};
const DEFAULT_SETTINGS_CONFIG: ClaudeCliSettingsConfig = {
version: '1.0.0',
defaultTool: 'gemini',
settings: {
promptFormat: 'plain',
smartContext: {
enabled: false,
@@ -91,8 +133,7 @@ const DEFAULT_CONFIG: ClaudeCliToolsConfig = {
defaultPrefix: '',
defaultSuffix: ''
},
codeIndexMcp: 'codexlens' // Default to CodexLens
}
codeIndexMcp: 'ace'
};
// ========== Helper Functions ==========
@@ -101,10 +142,18 @@ function getProjectConfigPath(projectDir: string): string {
return path.join(projectDir, '.claude', 'cli-tools.json');
}
function getProjectSettingsPath(projectDir: string): string {
return path.join(projectDir, '.claude', 'cli-settings.json');
}
function getGlobalConfigPath(): string {
return path.join(os.homedir(), '.claude', 'cli-tools.json');
}
function getGlobalSettingsPath(): string {
return path.join(os.homedir(), '.claude', 'cli-settings.json');
}
/**
* Resolve config path with fallback:
* 1. Project: {projectDir}/.claude/cli-tools.json
@@ -125,6 +174,25 @@ function resolveConfigPath(projectDir: string): { path: string; source: 'project
return { path: projectPath, source: 'default' };
}
/**
* Resolve settings path with fallback:
* 1. Project: {projectDir}/.claude/cli-settings.json
* 2. Global: ~/.claude/cli-settings.json
*/
function resolveSettingsPath(projectDir: string): { path: string; source: 'project' | 'global' | 'default' } {
const projectPath = getProjectSettingsPath(projectDir);
if (fs.existsSync(projectPath)) {
return { path: projectPath, source: 'project' };
}
const globalPath = getGlobalSettingsPath();
if (fs.existsSync(globalPath)) {
return { path: globalPath, source: 'global' };
}
return { path: projectPath, source: 'default' };
}
function ensureClaudeDir(projectDir: string): void {
const claudeDir = path.join(projectDir, '.claude');
if (!fs.existsSync(claudeDir)) {
@@ -134,6 +202,20 @@ function ensureClaudeDir(projectDir: string): void {
// ========== Main Functions ==========
/**
* Ensure tool has tags field (for backward compatibility)
*/
function ensureToolTags(tool: Partial<ClaudeCliTool>): ClaudeCliTool {
return {
enabled: tool.enabled ?? true,
isBuiltin: tool.isBuiltin ?? false,
command: tool.command ?? '',
description: tool.description ?? '',
primaryModel: tool.primaryModel,
tags: tool.tags ?? []
};
}
/**
* Load CLI tools configuration with fallback:
* 1. Project: {projectDir}/.claude/cli-tools.json
@@ -145,61 +227,115 @@ export function loadClaudeCliTools(projectDir: string): ClaudeCliToolsConfig & {
try {
if (resolved.source === 'default') {
// No config file found, return defaults
return { ...DEFAULT_CONFIG, _source: 'default' };
return { ...DEFAULT_TOOLS_CONFIG, _source: 'default' };
}
const content = fs.readFileSync(resolved.path, 'utf-8');
const parsed = JSON.parse(content) as Partial<ClaudeCliToolsConfig>;
const parsed = JSON.parse(content) as Partial<ClaudeCliCombinedConfig>;
// Merge with defaults
const config = {
...DEFAULT_CONFIG,
...parsed,
tools: { ...DEFAULT_CONFIG.tools, ...(parsed.tools || {}) },
settings: {
...DEFAULT_CONFIG.settings,
...(parsed.settings || {}),
smartContext: {
...DEFAULT_CONFIG.settings.smartContext,
...(parsed.settings?.smartContext || {})
},
cache: {
...DEFAULT_CONFIG.settings.cache,
...(parsed.settings?.cache || {})
// Merge tools with defaults and ensure tags exist
const mergedTools: Record<string, ClaudeCliTool> = {};
for (const [key, tool] of Object.entries({ ...DEFAULT_TOOLS_CONFIG.tools, ...(parsed.tools || {}) })) {
mergedTools[key] = ensureToolTags(tool);
}
},
// Ensure customEndpoints have tags
const mergedEndpoints = (parsed.customEndpoints || []).map(ep => ({
...ep,
tags: ep.tags ?? []
}));
const config: ClaudeCliToolsConfig & { _source?: string } = {
version: parsed.version || DEFAULT_TOOLS_CONFIG.version,
tools: mergedTools,
customEndpoints: mergedEndpoints,
$schema: parsed.$schema,
_source: resolved.source
};
console.log(`[claude-cli-tools] Loaded config from ${resolved.source}: ${resolved.path}`);
console.log(`[claude-cli-tools] Loaded tools config from ${resolved.source}: ${resolved.path}`);
return config;
} catch (err) {
console.error('[claude-cli-tools] Error loading config:', err);
return { ...DEFAULT_CONFIG, _source: 'default' };
console.error('[claude-cli-tools] Error loading tools config:', err);
return { ...DEFAULT_TOOLS_CONFIG, _source: 'default' };
}
}
/**
* Save CLI tools configuration to project .claude/cli-tools.json
* Always saves to project directory (not global)
*/
export function saveClaudeCliTools(projectDir: string, config: ClaudeCliToolsConfig & { _source?: string }): void {
ensureClaudeDir(projectDir);
const configPath = getProjectConfigPath(projectDir);
// Remove internal _source field before saving
const { _source, ...configToSave } = config;
try {
fs.writeFileSync(configPath, JSON.stringify(configToSave, null, 2), 'utf-8');
console.log(`[claude-cli-tools] Saved config to project: ${configPath}`);
console.log(`[claude-cli-tools] Saved tools config to: ${configPath}`);
} catch (err) {
console.error('[claude-cli-tools] Error saving config:', err);
console.error('[claude-cli-tools] Error saving tools config:', err);
throw new Error(`Failed to save CLI tools config: ${err}`);
}
}
/**
* Load CLI settings configuration with fallback:
* 1. Project: {projectDir}/.claude/cli-settings.json
* 2. Global: ~/.claude/cli-settings.json
* 3. Default settings
*/
export function loadClaudeCliSettings(projectDir: string): ClaudeCliSettingsConfig & { _source?: string } {
const resolved = resolveSettingsPath(projectDir);
try {
if (resolved.source === 'default') {
return { ...DEFAULT_SETTINGS_CONFIG, _source: 'default' };
}
const content = fs.readFileSync(resolved.path, 'utf-8');
const parsed = JSON.parse(content) as Partial<ClaudeCliSettingsConfig>;
const config: ClaudeCliSettingsConfig & { _source?: string } = {
...DEFAULT_SETTINGS_CONFIG,
...parsed,
smartContext: {
...DEFAULT_SETTINGS_CONFIG.smartContext,
...(parsed.smartContext || {})
},
cache: {
...DEFAULT_SETTINGS_CONFIG.cache,
...(parsed.cache || {})
},
_source: resolved.source
};
console.log(`[claude-cli-tools] Loaded settings from ${resolved.source}: ${resolved.path}`);
return config;
} catch (err) {
console.error('[claude-cli-tools] Error loading settings:', err);
return { ...DEFAULT_SETTINGS_CONFIG, _source: 'default' };
}
}
/**
* Save CLI settings configuration to project .claude/cli-settings.json
*/
export function saveClaudeCliSettings(projectDir: string, config: ClaudeCliSettingsConfig & { _source?: string }): void {
ensureClaudeDir(projectDir);
const settingsPath = getProjectSettingsPath(projectDir);
const { _source, ...configToSave } = config;
try {
fs.writeFileSync(settingsPath, JSON.stringify(configToSave, null, 2), 'utf-8');
console.log(`[claude-cli-tools] Saved settings to: ${settingsPath}`);
} catch (err) {
console.error('[claude-cli-tools] Error saving settings:', err);
throw new Error(`Failed to save CLI settings: ${err}`);
}
}
/**
* Update enabled status for a specific tool
*/
@@ -224,16 +360,16 @@ export function updateClaudeToolEnabled(
export function updateClaudeCacheSettings(
projectDir: string,
cacheSettings: Partial<ClaudeCacheSettings>
): ClaudeCliToolsConfig {
const config = loadClaudeCliTools(projectDir);
): ClaudeCliSettingsConfig {
const settings = loadClaudeCliSettings(projectDir);
config.settings.cache = {
...config.settings.cache,
settings.cache = {
...settings.cache,
...cacheSettings
};
saveClaudeCliTools(projectDir, config);
return config;
saveClaudeCliSettings(projectDir, settings);
return settings;
}
/**
@@ -242,11 +378,11 @@ export function updateClaudeCacheSettings(
export function updateClaudeDefaultTool(
projectDir: string,
defaultTool: string
): ClaudeCliToolsConfig {
const config = loadClaudeCliTools(projectDir);
config.defaultTool = defaultTool;
saveClaudeCliTools(projectDir, config);
return config;
): ClaudeCliSettingsConfig {
const settings = loadClaudeCliSettings(projectDir);
settings.defaultTool = defaultTool;
saveClaudeCliSettings(projectDir, settings);
return settings;
}
/**
@@ -254,16 +390,23 @@ export function updateClaudeDefaultTool(
*/
export function addClaudeCustomEndpoint(
projectDir: string,
endpoint: { id: string; name: string; enabled: boolean }
endpoint: { id: string; name: string; enabled: boolean; tags?: string[] }
): ClaudeCliToolsConfig {
const config = loadClaudeCliTools(projectDir);
const newEndpoint: ClaudeCustomEndpoint = {
id: endpoint.id,
name: endpoint.name,
enabled: endpoint.enabled,
tags: endpoint.tags || []
};
// Check if endpoint already exists
const existingIndex = config.customEndpoints.findIndex(e => e.id === endpoint.id);
if (existingIndex >= 0) {
config.customEndpoints[existingIndex] = endpoint;
config.customEndpoints[existingIndex] = newEndpoint;
} else {
config.customEndpoints.push(endpoint);
config.customEndpoints.push(newEndpoint);
}
saveClaudeCliTools(projectDir, config);
@@ -309,12 +452,12 @@ export function getClaudeCliToolsInfo(projectDir: string): {
export function updateCodeIndexMcp(
projectDir: string,
provider: 'codexlens' | 'ace' | 'none'
): { success: boolean; error?: string; config?: ClaudeCliToolsConfig } {
): { success: boolean; error?: string; settings?: ClaudeCliSettingsConfig } {
try {
// Update config
const config = loadClaudeCliTools(projectDir);
config.settings.codeIndexMcp = provider;
saveClaudeCliTools(projectDir, config);
// Update settings config
const settings = loadClaudeCliSettings(projectDir);
settings.codeIndexMcp = provider;
saveClaudeCliSettings(projectDir, settings);
// Only update global CLAUDE.md (consistent with Chinese response / Windows platform)
const globalClaudeMdPath = path.join(os.homedir(), '.claude', 'CLAUDE.md');
@@ -358,7 +501,7 @@ export function updateCodeIndexMcp(
console.log(`[claude-cli-tools] Updated global CLAUDE.md to use ${provider}`);
}
return { success: true, config };
return { success: true, settings };
} catch (err) {
console.error('[claude-cli-tools] Error updating Code Index MCP:', err);
return { success: false, error: (err as Error).message };
@@ -369,8 +512,8 @@ export function updateCodeIndexMcp(
* Get current Code Index MCP provider
*/
export function getCodeIndexMcp(projectDir: string): 'codexlens' | 'ace' | 'none' {
const config = loadClaudeCliTools(projectDir);
return config.settings.codeIndexMcp || 'codexlens';
const settings = loadClaudeCliSettings(projectDir);
return settings.codeIndexMcp || 'ace';
}
/**

View File

@@ -6,6 +6,7 @@
import * as fs from 'fs';
import * as path from 'path';
import { StoragePaths, ensureStorageDir } from '../config/storage-paths.js';
import { loadClaudeCliTools, saveClaudeCliTools } from './claude-cli-tools.js';
// ========== Types ==========
@@ -234,6 +235,20 @@ export function updateToolConfig(
config.tools[tool] = updatedToolConfig;
saveCliConfig(baseDir, config);
// Also sync tags to cli-tools.json
if (updates.tags !== undefined) {
try {
const claudeCliTools = loadClaudeCliTools(baseDir);
if (claudeCliTools.tools[tool]) {
claudeCliTools.tools[tool].tags = updatedToolConfig.tags || [];
saveClaudeCliTools(baseDir, claudeCliTools);
}
} catch (err) {
// Log warning instead of ignoring errors syncing to cli-tools.json
console.warn(`[cli-config] Failed to sync tags to cli-tools.json for tool '${tool}'.`, err);
}
}
return updatedToolConfig;
}
@@ -298,14 +313,30 @@ export function getPredefinedModels(tool: string): string[] {
}
/**
* Get full config response for API (includes predefined models)
* Get full config response for API (includes predefined models and tags from cli-tools.json)
*/
export function getFullConfigResponse(baseDir: string): {
config: CliConfig;
predefinedModels: Record<string, string[]>;
} {
const config = loadCliConfig(baseDir);
// Merge tags from cli-tools.json
try {
const claudeCliTools = loadClaudeCliTools(baseDir);
for (const [toolName, toolConfig] of Object.entries(config.tools)) {
const claudeTool = claudeCliTools.tools[toolName];
if (claudeTool && claudeTool.tags) {
toolConfig.tags = claudeTool.tags;
}
}
} catch (err) {
// Log warning instead of ignoring errors loading cli-tools.json
console.warn('[cli-config] Could not merge tags from cli-tools.json.', err);
}
return {
config: loadCliConfig(baseDir),
config,
predefinedModels: { ...PREDEFINED_MODELS }
};
}

View File

@@ -353,6 +353,7 @@ export interface CodexLensEmbeddingRotation {
/**
* Generic embedding pool configuration (refactored from CodexLensEmbeddingRotation)
* Supports automatic discovery of all providers offering a specific model
* @deprecated Use ModelPoolConfig instead
*/
export interface EmbeddingPoolConfig {
/** Whether embedding pool is enabled */
@@ -377,6 +378,50 @@ export interface EmbeddingPoolConfig {
defaultMaxConcurrentPerKey: number;
}
/**
* Model type for pool configuration
*/
export type ModelPoolType = 'embedding' | 'llm' | 'reranker';
/**
* Individual model pool configuration
* Supports embedding, LLM, and reranker models with high availability
*/
export interface ModelPoolConfig {
/** Unique identifier for this pool */
id: string;
/** Model type: embedding, llm, or reranker */
modelType: ModelPoolType;
/** Whether this pool is enabled */
enabled: boolean;
/** Target model name (e.g., "text-embedding-3-small", "gpt-4o") */
targetModel: string;
/** Selection strategy: round_robin, latency_aware, weighted_random */
strategy: 'round_robin' | 'latency_aware' | 'weighted_random';
/** Whether to automatically discover all providers offering targetModel */
autoDiscover: boolean;
/** Provider IDs to exclude from auto-discovery (optional) */
excludedProviderIds?: string[];
/** Default cooldown seconds for rate-limited endpoints (default: 60) */
defaultCooldown: number;
/** Default maximum concurrent requests per key (default: 4) */
defaultMaxConcurrentPerKey: number;
/** Optional display name for this pool */
name?: string;
/** Optional description */
description?: string;
}
/**
* Complete LiteLLM API configuration
* Root configuration object stored in JSON file
@@ -400,6 +445,9 @@ export interface LiteLLMApiConfig {
/** CodexLens multi-provider embedding rotation config (deprecated, use embeddingPoolConfig) */
codexlensEmbeddingRotation?: CodexLensEmbeddingRotation;
/** Generic embedding pool configuration with auto-discovery support */
/** Generic embedding pool configuration with auto-discovery support (deprecated, use modelPools) */
embeddingPoolConfig?: EmbeddingPoolConfig;
/** Multi-model pool configurations (supports embedding, LLM, reranker) */
modelPools?: ModelPoolConfig[];
}