mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-04 01:40:45 +08:00
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:
18
.claude/cli-settings.json
Normal file
18
.claude/cli-settings.json
Normal 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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 || {}),
|
||||
// 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: {
|
||||
...config.settings.cache,
|
||||
...(updates.settings?.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 };
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -550,15 +550,15 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
|
||||
|
||||
// Serve dashboard HTML
|
||||
if (pathname === '/' || pathname === '/index.html') {
|
||||
if (isLocalhostRequest(req)) {
|
||||
const tokenResult = tokenManager.getOrCreateAuthToken();
|
||||
setAuthCookie(res, tokenResult.token, tokenResult.expiresAt);
|
||||
// Set session cookie and CSRF token for all requests
|
||||
const tokenResult = tokenManager.getOrCreateAuthToken();
|
||||
setAuthCookie(res, tokenResult.token, tokenResult.expiresAt);
|
||||
|
||||
const sessionId = getOrCreateSessionId(req, res);
|
||||
const csrfToken = getCsrfTokenManager().generateToken(sessionId);
|
||||
res.setHeader('X-CSRF-Token', csrfToken);
|
||||
setCsrfCookie(res, csrfToken, 15 * 60);
|
||||
|
||||
const sessionId = getOrCreateSessionId(req, res);
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -292,11 +355,11 @@ async function toggleProviderExclusion(providerId) {
|
||||
|
||||
// Re-render the discovered providers section
|
||||
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()">×</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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,76 +33,107 @@ 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;
|
||||
maxFiles: number;
|
||||
promptFormat: 'plain' | 'yaml' | 'json';
|
||||
smartContext: {
|
||||
enabled: boolean;
|
||||
maxFiles: number;
|
||||
};
|
||||
nativeResume: boolean;
|
||||
recursiveQuery: boolean;
|
||||
cache: ClaudeCacheSettings;
|
||||
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: ClaudeCacheSettings;
|
||||
codeIndexMcp: 'codexlens' | 'ace' | 'none'; // Code Index MCP provider
|
||||
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,
|
||||
maxFiles: 10
|
||||
},
|
||||
nativeResume: true,
|
||||
recursiveQuery: true,
|
||||
cache: {
|
||||
injectionMode: 'auto',
|
||||
defaultPrefix: '',
|
||||
defaultSuffix: ''
|
||||
},
|
||||
codeIndexMcp: 'codexlens' // Default to CodexLens
|
||||
}
|
||||
promptFormat: 'plain',
|
||||
smartContext: {
|
||||
enabled: false,
|
||||
maxFiles: 10
|
||||
},
|
||||
nativeResume: true,
|
||||
recursiveQuery: true,
|
||||
cache: {
|
||||
injectionMode: 'auto',
|
||||
defaultPrefix: '',
|
||||
defaultSuffix: ''
|
||||
},
|
||||
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';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user