mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +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": {
|
"tools": {
|
||||||
"gemini": {
|
"gemini": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"isBuiltin": true,
|
"isBuiltin": true,
|
||||||
"command": "gemini",
|
"command": "gemini",
|
||||||
"description": "Google AI for code analysis"
|
"description": "Google AI for code analysis",
|
||||||
|
"tags": []
|
||||||
},
|
},
|
||||||
"qwen": {
|
"qwen": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"isBuiltin": true,
|
"isBuiltin": true,
|
||||||
"command": "qwen",
|
"command": "qwen",
|
||||||
"description": "Alibaba AI assistant"
|
"description": "Alibaba AI assistant",
|
||||||
|
"tags": []
|
||||||
},
|
},
|
||||||
"codex": {
|
"codex": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"isBuiltin": true,
|
"isBuiltin": true,
|
||||||
"command": "codex",
|
"command": "codex",
|
||||||
"description": "OpenAI code generation"
|
"description": "OpenAI code generation",
|
||||||
|
"tags": []
|
||||||
},
|
},
|
||||||
"claude": {
|
"claude": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"isBuiltin": true,
|
"isBuiltin": true,
|
||||||
"command": "claude",
|
"command": "claude",
|
||||||
"description": "Anthropic AI assistant"
|
"description": "Anthropic AI assistant",
|
||||||
|
"tags": []
|
||||||
},
|
},
|
||||||
"opencode": {
|
"opencode": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"isBuiltin": true,
|
"isBuiltin": true,
|
||||||
"command": "opencode",
|
"command": "opencode",
|
||||||
"description": "OpenCode AI assistant",
|
"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",
|
"defaultTool": "gemini",
|
||||||
"settings": {
|
"settings": {
|
||||||
"promptFormat": "plain",
|
"promptFormat": "plain",
|
||||||
@@ -48,7 +59,7 @@
|
|||||||
"defaultPrefix": "",
|
"defaultPrefix": "",
|
||||||
"defaultSuffix": ""
|
"defaultSuffix": ""
|
||||||
},
|
},
|
||||||
"codeIndexMcp": "ace"
|
"codeIndexMcp": "codexlens"
|
||||||
},
|
},
|
||||||
"$schema": "./cli-tools.schema.json"
|
"$schema": "./cli-tools.schema.json"
|
||||||
}
|
}
|
||||||
@@ -1033,5 +1033,219 @@ function objectToYaml(obj: unknown, indent: number = 0): string {
|
|||||||
return String(obj);
|
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
|
// Re-export types
|
||||||
export type { ProviderCredential, CustomEndpoint, ProviderType, CacheStrategy, CodexLensEmbeddingRotation, CodexLensEmbeddingProvider, EmbeddingPoolConfig };
|
export type { ProviderCredential, CustomEndpoint, ProviderType, CacheStrategy, CodexLensEmbeddingRotation, CodexLensEmbeddingProvider, EmbeddingPoolConfig };
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
loadClaudeCliTools,
|
loadClaudeCliTools,
|
||||||
saveClaudeCliTools,
|
saveClaudeCliTools,
|
||||||
|
loadClaudeCliSettings,
|
||||||
|
saveClaudeCliSettings,
|
||||||
updateClaudeToolEnabled,
|
updateClaudeToolEnabled,
|
||||||
updateClaudeCacheSettings,
|
updateClaudeCacheSettings,
|
||||||
getClaudeCliToolsInfo,
|
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)
|
// API: Get CLI Tools Config from .claude/cli-tools.json (with fallback to global)
|
||||||
if (pathname === '/api/cli/tools-config' && req.method === 'GET') {
|
if (pathname === '/api/cli/tools-config' && req.method === 'GET') {
|
||||||
try {
|
try {
|
||||||
const config = loadClaudeCliTools(initialPath);
|
const toolsConfig = loadClaudeCliTools(initialPath);
|
||||||
|
const settingsConfig = loadClaudeCliSettings(initialPath);
|
||||||
const info = getClaudeCliToolsInfo(initialPath);
|
const info = getClaudeCliToolsInfo(initialPath);
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
res.end(JSON.stringify({
|
res.end(JSON.stringify({
|
||||||
...config,
|
tools: toolsConfig,
|
||||||
|
settings: settingsConfig,
|
||||||
_configInfo: info
|
_configInfo: info
|
||||||
}));
|
}));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -722,32 +726,55 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
|
|||||||
if (pathname === '/api/cli/tools-config' && req.method === 'PUT') {
|
if (pathname === '/api/cli/tools-config' && req.method === 'PUT') {
|
||||||
handlePostRequest(req, res, async (body: unknown) => {
|
handlePostRequest(req, res, async (body: unknown) => {
|
||||||
try {
|
try {
|
||||||
const updates = body as Partial<any>;
|
const updates = body as { tools?: any; settings?: any };
|
||||||
const config = loadClaudeCliTools(initialPath);
|
|
||||||
|
|
||||||
// Merge updates
|
// Update tools config if provided
|
||||||
const updatedConfig = {
|
if (updates.tools) {
|
||||||
...config,
|
const currentTools = loadClaudeCliTools(initialPath);
|
||||||
...updates,
|
const updatedTools = {
|
||||||
tools: { ...config.tools, ...(updates.tools || {}) },
|
...currentTools,
|
||||||
settings: {
|
tools: { ...currentTools.tools, ...(updates.tools.tools || {}) },
|
||||||
...config.settings,
|
customEndpoints: updates.tools.customEndpoints || currentTools.customEndpoints
|
||||||
...(updates.settings || {}),
|
};
|
||||||
|
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: {
|
cache: {
|
||||||
...config.settings.cache,
|
...currentSettings.cache,
|
||||||
...(updates.settings?.cache || {})
|
...(s.cache || {})
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
};
|
saveClaudeCliSettings(initialPath, updatedSettings);
|
||||||
|
}
|
||||||
|
|
||||||
saveClaudeCliTools(initialPath, updatedConfig);
|
const toolsConfig = loadClaudeCliTools(initialPath);
|
||||||
|
const settingsConfig = loadClaudeCliSettings(initialPath);
|
||||||
|
|
||||||
broadcastToClients({
|
broadcastToClients({
|
||||||
type: 'CLI_TOOLS_CONFIG_UPDATED',
|
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) {
|
} catch (err) {
|
||||||
return { error: (err as Error).message, status: 500 };
|
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) => {
|
handlePostRequest(req, res, async (body: unknown) => {
|
||||||
try {
|
try {
|
||||||
const cacheSettings = body as { injectionMode?: string; defaultPrefix?: string; defaultSuffix?: string };
|
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({
|
broadcastToClients({
|
||||||
type: 'CLI_CACHE_SETTINGS_UPDATED',
|
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) {
|
} catch (err) {
|
||||||
return { error: (err as Error).message, status: 500 };
|
return { error: (err as Error).message, status: 500 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,35 @@
|
|||||||
*/
|
*/
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { dirname, join as pathJoin } from 'path';
|
import { dirname, join as pathJoin } from 'path';
|
||||||
|
import { z } from 'zod';
|
||||||
import { getSystemPython } from '../../utils/python-utils.js';
|
import { getSystemPython } from '../../utils/python-utils.js';
|
||||||
import type { RouteContext } from './types.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
|
// Get current module path for package-relative lookups
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
@@ -39,6 +65,12 @@ import {
|
|||||||
getEmbeddingPoolConfig,
|
getEmbeddingPoolConfig,
|
||||||
updateEmbeddingPoolConfig,
|
updateEmbeddingPoolConfig,
|
||||||
discoverProvidersForModel,
|
discoverProvidersForModel,
|
||||||
|
getModelPools,
|
||||||
|
getModelPool,
|
||||||
|
addModelPool,
|
||||||
|
updateModelPool,
|
||||||
|
deleteModelPool,
|
||||||
|
getAvailableModelsForType,
|
||||||
type ProviderCredential,
|
type ProviderCredential,
|
||||||
type CustomEndpoint,
|
type CustomEndpoint,
|
||||||
type ProviderType,
|
type ProviderType,
|
||||||
@@ -856,6 +888,186 @@ export async function handleLiteLLMApiRoutes(ctx: RouteContext): Promise<boolean
|
|||||||
return true;
|
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
|
// POST /api/litellm-api/ccw-litellm/install - Install ccw-litellm package
|
||||||
if (pathname === '/api/litellm-api/ccw-litellm/install' && req.method === 'POST') {
|
if (pathname === '/api/litellm-api/ccw-litellm/install' && req.method === 'POST') {
|
||||||
handlePostRequest(req, res, async () => {
|
handlePostRequest(req, res, async () => {
|
||||||
|
|||||||
@@ -550,15 +550,15 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
|
|||||||
|
|
||||||
// Serve dashboard HTML
|
// Serve dashboard HTML
|
||||||
if (pathname === '/' || pathname === '/index.html') {
|
if (pathname === '/' || pathname === '/index.html') {
|
||||||
if (isLocalhostRequest(req)) {
|
// Set session cookie and CSRF token for all requests
|
||||||
const tokenResult = tokenManager.getOrCreateAuthToken();
|
const tokenResult = tokenManager.getOrCreateAuthToken();
|
||||||
setAuthCookie(res, tokenResult.token, tokenResult.expiresAt);
|
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);
|
const html = generateServerDashboard(initialPath);
|
||||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||||
res.end(html);
|
res.end(html);
|
||||||
|
|||||||
@@ -972,27 +972,31 @@ select.cli-input {
|
|||||||
|
|
||||||
.sidebar-tabs {
|
.sidebar-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0;
|
flex-wrap: nowrap;
|
||||||
|
gap: 0.125rem;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
background: hsl(var(--muted) / 0.3);
|
background: hsl(var(--muted) / 0.3);
|
||||||
border-bottom: 1px solid hsl(var(--border));
|
border-bottom: 1px solid hsl(var(--border));
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-tab {
|
.sidebar-tab {
|
||||||
flex: 1;
|
flex: 1 1 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 0.25rem;
|
gap: 0.125rem;
|
||||||
padding: 0.5rem 0.25rem;
|
padding: 0.5rem 0.25rem;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: hsl(var(--muted-foreground));
|
color: hsl(var(--muted-foreground));
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: 1px solid transparent;
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.5rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-tab:hover {
|
.sidebar-tab:hover {
|
||||||
@@ -1002,12 +1006,14 @@ select.cli-input {
|
|||||||
|
|
||||||
.sidebar-tab.active {
|
.sidebar-tab.active {
|
||||||
color: hsl(var(--primary));
|
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 {
|
.sidebar-tab i {
|
||||||
width: 14px;
|
width: 18px;
|
||||||
height: 14px;
|
height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sidebar content areas */
|
/* Sidebar content areas */
|
||||||
@@ -1107,13 +1113,13 @@ select.cli-input {
|
|||||||
/* Responsive adjustments for tabs */
|
/* Responsive adjustments for tabs */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.sidebar-tab {
|
.sidebar-tab {
|
||||||
padding: 0.5rem 0.5rem;
|
padding: 0.5rem 0.625rem;
|
||||||
font-size: 0.7rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-tab i {
|
.sidebar-tab i {
|
||||||
width: 12px;
|
width: 16px;
|
||||||
height: 12px;
|
height: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,13 +11,19 @@ let selectedProviderId = null;
|
|||||||
let providerSearchQuery = '';
|
let providerSearchQuery = '';
|
||||||
let activeModelTab = 'llm';
|
let activeModelTab = 'llm';
|
||||||
let expandedModelGroups = new Set();
|
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 embeddingPoolConfig = null;
|
||||||
let embeddingPoolAvailableModels = [];
|
let embeddingPoolAvailableModels = [];
|
||||||
let embeddingPoolDiscoveredProviders = [];
|
let embeddingPoolDiscoveredProviders = [];
|
||||||
|
|
||||||
|
// Multi-Model Pool state
|
||||||
|
let modelPools = [];
|
||||||
|
let selectedPoolId = null;
|
||||||
|
let poolAvailableModels = {};
|
||||||
|
let poolDiscoveredProviders = {};
|
||||||
|
|
||||||
// CLI Settings state
|
// CLI Settings state
|
||||||
let cliSettingsData = null;
|
let cliSettingsData = null;
|
||||||
let selectedCliSettingsId = 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)
|
// Track if this is the first render (force refresh on first load)
|
||||||
let isFirstApiSettingsRender = true;
|
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 ==========
|
// ========== 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
|
* Save CLI Settings endpoint
|
||||||
*/
|
*/
|
||||||
@@ -138,7 +198,8 @@ async function saveCliSettingsEndpoint(data) {
|
|||||||
const method = data.id ? 'PUT' : 'POST';
|
const method = data.id ? 'PUT' : 'POST';
|
||||||
const url = data.id ? '/api/cli/settings/' + data.id : '/api/cli/settings';
|
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,
|
method: method,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data)
|
||||||
@@ -174,7 +235,8 @@ async function deleteCliSettingsEndpoint(endpointId) {
|
|||||||
if (!confirm(t('apiSettings.confirmDeleteSettings'))) return;
|
if (!confirm(t('apiSettings.confirmDeleteSettings'))) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/cli/settings/' + endpointId, {
|
await initCsrfToken();
|
||||||
|
const response = await csrfFetch('/api/cli/settings/' + endpointId, {
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -237,7 +299,8 @@ async function saveEmbeddingPoolConfig() {
|
|||||||
defaultMaxConcurrentPerKey: defaultMaxConcurrentPerKey
|
defaultMaxConcurrentPerKey: defaultMaxConcurrentPerKey
|
||||||
} : null;
|
} : null;
|
||||||
|
|
||||||
const response = await fetch('/api/litellm-api/embedding-pool', {
|
await initCsrfToken();
|
||||||
|
const response = await csrfFetch('/api/litellm-api/embedding-pool', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(poolConfig)
|
body: JSON.stringify(poolConfig)
|
||||||
@@ -260,9 +323,9 @@ async function saveEmbeddingPoolConfig() {
|
|||||||
// Update sidebar summary
|
// Update sidebar summary
|
||||||
const sidebarContainer = document.querySelector('.api-settings-sidebar');
|
const sidebarContainer = document.querySelector('.api-settings-sidebar');
|
||||||
if (sidebarContainer) {
|
if (sidebarContainer) {
|
||||||
const contentArea = sidebarContainer.querySelector('.provider-list, .endpoints-list, .embedding-pool-sidebar-info, .embedding-pool-sidebar-summary, .cache-sidebar-info');
|
const contentArea = sidebarContainer.querySelector('.embedding-pool-sidebar-info, .embedding-pool-sidebar-summary');
|
||||||
if (contentArea && contentArea.parentElement) {
|
if (contentArea) {
|
||||||
contentArea.parentElement.innerHTML = renderEmbeddingPoolSidebar();
|
contentArea.outerHTML = renderEmbeddingPoolSidebar();
|
||||||
if (window.lucide) lucide.createIcons();
|
if (window.lucide) lucide.createIcons();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -294,9 +357,9 @@ async function toggleProviderExclusion(providerId) {
|
|||||||
renderDiscoveredProviders();
|
renderDiscoveredProviders();
|
||||||
|
|
||||||
// Update sidebar summary
|
// Update sidebar summary
|
||||||
const sidebarContainer = document.querySelector('.api-settings-sidebar .embedding-pool-sidebar-summary');
|
const sidebarContainer = document.querySelector('.api-settings-sidebar .embedding-pool-sidebar-summary, .api-settings-sidebar .embedding-pool-sidebar-info');
|
||||||
if (sidebarContainer && sidebarContainer.parentElement) {
|
if (sidebarContainer) {
|
||||||
sidebarContainer.parentElement.innerHTML = renderEmbeddingPoolSidebar();
|
sidebarContainer.outerHTML = renderEmbeddingPoolSidebar();
|
||||||
if (window.lucide) lucide.createIcons();
|
if (window.lucide) lucide.createIcons();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -557,7 +620,8 @@ async function saveProvider() {
|
|||||||
: '/api/litellm-api/providers';
|
: '/api/litellm-api/providers';
|
||||||
const method = providerId ? 'PUT' : 'POST';
|
const method = providerId ? 'PUT' : 'POST';
|
||||||
|
|
||||||
const response = await fetch(url, {
|
await initCsrfToken();
|
||||||
|
const response = await csrfFetch(url, {
|
||||||
method: method,
|
method: method,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(providerData)
|
body: JSON.stringify(providerData)
|
||||||
@@ -585,7 +649,8 @@ async function deleteProvider(providerId) {
|
|||||||
if (!confirm(t('apiSettings.confirmDeleteProvider'))) return;
|
if (!confirm(t('apiSettings.confirmDeleteProvider'))) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/litellm-api/providers/' + providerId, {
|
await initCsrfToken();
|
||||||
|
const response = await csrfFetch('/api/litellm-api/providers/' + providerId, {
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -624,7 +689,8 @@ async function testProviderConnection(providerIdParam) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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'
|
method: 'POST'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -917,7 +983,8 @@ async function saveEndpoint() {
|
|||||||
: '/api/litellm-api/endpoints';
|
: '/api/litellm-api/endpoints';
|
||||||
const method = form.dataset.endpointId ? 'PUT' : 'POST';
|
const method = form.dataset.endpointId ? 'PUT' : 'POST';
|
||||||
|
|
||||||
const response = await fetch(url, {
|
await initCsrfToken();
|
||||||
|
const response = await csrfFetch(url, {
|
||||||
method: method,
|
method: method,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(endpointData)
|
body: JSON.stringify(endpointData)
|
||||||
@@ -945,7 +1012,8 @@ async function deleteEndpoint(endpointId) {
|
|||||||
if (!confirm(t('apiSettings.confirmDeleteEndpoint'))) return;
|
if (!confirm(t('apiSettings.confirmDeleteEndpoint'))) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/litellm-api/endpoints/' + endpointId, {
|
await initCsrfToken();
|
||||||
|
const response = await csrfFetch('/api/litellm-api/endpoints/' + endpointId, {
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1018,7 +1086,8 @@ async function clearCache() {
|
|||||||
if (!confirm(t('apiSettings.confirmClearCache'))) return;
|
if (!confirm(t('apiSettings.confirmClearCache'))) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/litellm-api/cache/clear', {
|
await initCsrfToken();
|
||||||
|
const response = await csrfFetch('/api/litellm-api/cache/clear', {
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1042,7 +1111,8 @@ async function toggleGlobalCache() {
|
|||||||
const enabled = document.getElementById('global-cache-enabled').checked;
|
const enabled = document.getElementById('global-cache-enabled').checked;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/litellm-api/config/cache', {
|
await initCsrfToken();
|
||||||
|
const response = await csrfFetch('/api/litellm-api/config/cache', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ enabled: enabled })
|
body: JSON.stringify({ enabled: enabled })
|
||||||
@@ -1095,8 +1165,11 @@ async function renderApiSettings() {
|
|||||||
'<button class="sidebar-tab' + (activeSidebarTab === 'cli-settings' ? ' active' : '') + '" onclick="switchSidebarTab(\'cli-settings\')">' +
|
'<button class="sidebar-tab' + (activeSidebarTab === 'cli-settings' ? ' active' : '') + '" onclick="switchSidebarTab(\'cli-settings\')">' +
|
||||||
'<i data-lucide="settings"></i> ' + t('apiSettings.cliSettings') +
|
'<i data-lucide="settings"></i> ' + t('apiSettings.cliSettings') +
|
||||||
'</button>' +
|
'</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\')">' +
|
'<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>' +
|
||||||
'<button class="sidebar-tab' + (activeSidebarTab === 'cache' ? ' active' : '') + '" onclick="switchSidebarTab(\'cache\')">' +
|
'<button class="sidebar-tab' + (activeSidebarTab === 'cache' ? ' active' : '') + '" onclick="switchSidebarTab(\'cache\')">' +
|
||||||
'<i data-lucide="database"></i> ' + t('apiSettings.cache') +
|
'<i data-lucide="database"></i> ' + t('apiSettings.cache') +
|
||||||
@@ -1127,6 +1200,15 @@ async function renderApiSettings() {
|
|||||||
await loadEmbeddingPoolConfig();
|
await loadEmbeddingPoolConfig();
|
||||||
}
|
}
|
||||||
sidebarContentHtml = renderEmbeddingPoolSidebar();
|
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') {
|
} else if (activeSidebarTab === 'cache') {
|
||||||
sidebarContentHtml = '<div class="cache-sidebar-info" style="padding: 1rem; color: var(--text-secondary); font-size: 0.875rem;">' +
|
sidebarContentHtml = '<div class="cache-sidebar-info" style="padding: 1rem; color: var(--text-secondary); font-size: 0.875rem;">' +
|
||||||
'<p>' + t('apiSettings.cacheTabHint') + '</p>' +
|
'<p>' + t('apiSettings.cacheTabHint') + '</p>' +
|
||||||
@@ -1177,6 +1259,16 @@ async function renderApiSettings() {
|
|||||||
renderEndpointsMainPanel();
|
renderEndpointsMainPanel();
|
||||||
} else if (activeSidebarTab === 'embedding-pool') {
|
} else if (activeSidebarTab === 'embedding-pool') {
|
||||||
renderEmbeddingPoolMainPanel();
|
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') {
|
} else if (activeSidebarTab === 'cache') {
|
||||||
renderCacheMainPanel();
|
renderCacheMainPanel();
|
||||||
} else if (activeSidebarTab === 'cli-settings') {
|
} else if (activeSidebarTab === 'cli-settings') {
|
||||||
@@ -1574,7 +1666,8 @@ function getDefaultApiBase(type) {
|
|||||||
*/
|
*/
|
||||||
async function toggleProviderEnabled(providerId, enabled) {
|
async function toggleProviderEnabled(providerId, enabled) {
|
||||||
try {
|
try {
|
||||||
var response = await fetch('/api/litellm-api/providers/' + providerId, {
|
await initCsrfToken();
|
||||||
|
var response = await csrfFetch('/api/litellm-api/providers/' + providerId, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ enabled: enabled })
|
body: JSON.stringify({ enabled: enabled })
|
||||||
@@ -2015,7 +2108,7 @@ function saveNewModel(event, providerId, modelType) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
models.push(newModel);
|
models.push(newModel);
|
||||||
return fetch('/api/litellm-api/providers/' + providerId, {
|
return csrfFetch('/api/litellm-api/providers/' + providerId, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ [modelsKey]: models })
|
body: JSON.stringify({ [modelsKey]: models })
|
||||||
@@ -2260,7 +2353,7 @@ function saveModelSettings(event, providerId, modelId, modelType) {
|
|||||||
var updateData = {};
|
var updateData = {};
|
||||||
updateData[modelsKey] = models;
|
updateData[modelsKey] = models;
|
||||||
|
|
||||||
return fetch('/api/litellm-api/providers/' + providerId, {
|
return csrfFetch('/api/litellm-api/providers/' + providerId, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(updateData)
|
body: JSON.stringify(updateData)
|
||||||
@@ -2299,7 +2392,7 @@ function deleteModel(providerId, modelId, modelType) {
|
|||||||
var updateData = {};
|
var updateData = {};
|
||||||
updateData[modelsKey] = updatedModels;
|
updateData[modelsKey] = updatedModels;
|
||||||
|
|
||||||
return fetch('/api/litellm-api/providers/' + providerId, {
|
return csrfFetch('/api/litellm-api/providers/' + providerId, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(updateData)
|
body: JSON.stringify(updateData)
|
||||||
@@ -2342,7 +2435,8 @@ async function saveProviderApiBase(providerId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var response = await fetch('/api/litellm-api/providers/' + providerId, {
|
await initCsrfToken();
|
||||||
|
var response = await csrfFetch('/api/litellm-api/providers/' + providerId, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ apiBase: newApiBase || undefined })
|
body: JSON.stringify({ apiBase: newApiBase || undefined })
|
||||||
@@ -2394,7 +2488,8 @@ async function deleteProviderWithConfirm(providerId) {
|
|||||||
if (!confirm(t('apiSettings.confirmDeleteProvider'))) return;
|
if (!confirm(t('apiSettings.confirmDeleteProvider'))) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var response = await fetch('/api/litellm-api/providers/' + providerId, {
|
await initCsrfToken();
|
||||||
|
var response = await csrfFetch('/api/litellm-api/providers/' + providerId, {
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2428,7 +2523,8 @@ async function deleteProviderWithConfirm(providerId) {
|
|||||||
*/
|
*/
|
||||||
async function syncConfigToCodexLens() {
|
async function syncConfigToCodexLens() {
|
||||||
try {
|
try {
|
||||||
var response = await fetch('/api/litellm-api/config/sync', {
|
await initCsrfToken();
|
||||||
|
var response = await csrfFetch('/api/litellm-api/config/sync', {
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2992,9 +3088,10 @@ async function onTargetModelChange(modelId) {
|
|||||||
// Update sidebar summary
|
// Update sidebar summary
|
||||||
const sidebarContainer = document.querySelector('.api-settings-sidebar');
|
const sidebarContainer = document.querySelector('.api-settings-sidebar');
|
||||||
if (sidebarContainer) {
|
if (sidebarContainer) {
|
||||||
const contentArea = sidebarContainer.querySelector('.provider-list, .endpoints-list, .embedding-pool-sidebar-info, .embedding-pool-sidebar-summary, .cache-sidebar-info');
|
const contentArea = sidebarContainer.querySelector('.provider-list, .endpoints-list, .embedding-pool-sidebar-info, .embedding-pool-sidebar-summary, .cache-sidebar-info, .cli-settings-list');
|
||||||
if (contentArea && contentArea.parentElement) {
|
if (contentArea) {
|
||||||
contentArea.parentElement.innerHTML = renderEmbeddingPoolSidebar();
|
// Use outerHTML to replace only the content area, not the entire sidebar
|
||||||
|
contentArea.outerHTML = renderEmbeddingPoolSidebar();
|
||||||
if (window.lucide) lucide.createIcons();
|
if (window.lucide) lucide.createIcons();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3253,7 +3350,7 @@ function addApiKey(providerId) {
|
|||||||
.then(function(provider) {
|
.then(function(provider) {
|
||||||
const apiKeys = provider.apiKeys || [];
|
const apiKeys = provider.apiKeys || [];
|
||||||
apiKeys.push(newKey);
|
apiKeys.push(newKey);
|
||||||
return fetch('/api/litellm-api/providers/' + providerId, {
|
return csrfFetch('/api/litellm-api/providers/' + providerId, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ apiKeys: apiKeys })
|
body: JSON.stringify({ apiKeys: apiKeys })
|
||||||
@@ -3279,7 +3376,7 @@ function removeApiKey(providerId, keyId) {
|
|||||||
.then(function(res) { return res.json(); })
|
.then(function(res) { return res.json(); })
|
||||||
.then(function(provider) {
|
.then(function(provider) {
|
||||||
const apiKeys = (provider.apiKeys || []).filter(function(k) { return k.id !== keyId; });
|
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',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ apiKeys: apiKeys })
|
body: JSON.stringify({ apiKeys: apiKeys })
|
||||||
@@ -3307,7 +3404,7 @@ function updateApiKeyField(providerId, keyId, field, value) {
|
|||||||
if (keyIndex >= 0) {
|
if (keyIndex >= 0) {
|
||||||
apiKeys[keyIndex][field] = value;
|
apiKeys[keyIndex][field] = value;
|
||||||
}
|
}
|
||||||
return fetch('/api/litellm-api/providers/' + providerId, {
|
return csrfFetch('/api/litellm-api/providers/' + providerId, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ apiKeys: apiKeys })
|
body: JSON.stringify({ apiKeys: apiKeys })
|
||||||
@@ -3322,7 +3419,7 @@ function updateApiKeyField(providerId, keyId, field, value) {
|
|||||||
* Update provider routing strategy
|
* Update provider routing strategy
|
||||||
*/
|
*/
|
||||||
function updateProviderRouting(providerId, strategy) {
|
function updateProviderRouting(providerId, strategy) {
|
||||||
fetch('/api/litellm-api/providers/' + providerId, {
|
csrfFetch('/api/litellm-api/providers/' + providerId, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ routingStrategy: strategy })
|
body: JSON.stringify({ routingStrategy: strategy })
|
||||||
@@ -3340,7 +3437,7 @@ function updateHealthCheckEnabled(providerId, enabled) {
|
|||||||
.then(function(provider) {
|
.then(function(provider) {
|
||||||
const healthCheck = provider.healthCheck || { intervalSeconds: 300, cooldownSeconds: 5, failureThreshold: 3 };
|
const healthCheck = provider.healthCheck || { intervalSeconds: 300, cooldownSeconds: 5, failureThreshold: 3 };
|
||||||
healthCheck.enabled = enabled;
|
healthCheck.enabled = enabled;
|
||||||
return fetch('/api/litellm-api/providers/' + providerId, {
|
return csrfFetch('/api/litellm-api/providers/' + providerId, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ healthCheck: healthCheck })
|
body: JSON.stringify({ healthCheck: healthCheck })
|
||||||
@@ -3365,7 +3462,7 @@ function updateHealthCheckField(providerId, field, value) {
|
|||||||
.then(function(provider) {
|
.then(function(provider) {
|
||||||
const healthCheck = provider.healthCheck || { enabled: false, intervalSeconds: 300, cooldownSeconds: 5, failureThreshold: 3 };
|
const healthCheck = provider.healthCheck || { enabled: false, intervalSeconds: 300, cooldownSeconds: 5, failureThreshold: 3 };
|
||||||
healthCheck[field] = value;
|
healthCheck[field] = value;
|
||||||
return fetch('/api/litellm-api/providers/' + providerId, {
|
return csrfFetch('/api/litellm-api/providers/' + providerId, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ healthCheck: healthCheck })
|
body: JSON.stringify({ healthCheck: healthCheck })
|
||||||
@@ -3385,7 +3482,7 @@ function testApiKey(providerId, keyId) {
|
|||||||
btn.classList.add('testing');
|
btn.classList.add('testing');
|
||||||
btn.textContent = t('apiSettings.testingKey');
|
btn.textContent = t('apiSettings.testingKey');
|
||||||
|
|
||||||
fetch('/api/litellm-api/providers/' + providerId + '/test-key', {
|
csrfFetch('/api/litellm-api/providers/' + providerId + '/test-key', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ keyId: keyId })
|
body: JSON.stringify({ keyId: keyId })
|
||||||
@@ -3525,7 +3622,8 @@ async function installCcwLitellm() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({})
|
body: JSON.stringify({})
|
||||||
@@ -3566,7 +3664,8 @@ async function uninstallCcwLitellm() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({})
|
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
|
// Make CLI Settings functions globally accessible
|
||||||
window.loadCliSettings = loadCliSettings;
|
window.loadCliSettings = loadCliSettings;
|
||||||
window.saveCliSettingsEndpoint = saveCliSettingsEndpoint;
|
window.saveCliSettingsEndpoint = saveCliSettingsEndpoint;
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ async function csrfFetch(url, options) {
|
|||||||
|
|
||||||
// Add CSRF token header for state-changing methods
|
// Add CSRF token header for state-changing methods
|
||||||
var method = (options.method || 'GET').toUpperCase();
|
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) {
|
if (['POST', 'PUT', 'PATCH', 'DELETE'].indexOf(method) !== -1 && csrfToken) {
|
||||||
options.headers['X-CSRF-Token'] = csrfToken;
|
options.headers['X-CSRF-Token'] = csrfToken;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* Claude CLI Tools Configuration Manager
|
* Claude CLI Tools Configuration Manager
|
||||||
* Manages .claude/cli-tools.json with fallback:
|
* Manages .claude/cli-tools.json (tools) and .claude/cli-settings.json (settings)
|
||||||
* 1. Project workspace: {projectDir}/.claude/cli-tools.json (priority)
|
* with fallback:
|
||||||
* 2. Global: ~/.claude/cli-tools.json (fallback)
|
* 1. Project workspace: {projectDir}/.claude/ (priority)
|
||||||
|
* 2. Global: ~/.claude/ (fallback)
|
||||||
*/
|
*/
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
@@ -15,6 +16,15 @@ export interface ClaudeCliTool {
|
|||||||
isBuiltin: boolean;
|
isBuiltin: boolean;
|
||||||
command: string;
|
command: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
primaryModel?: string;
|
||||||
|
tags: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClaudeCustomEndpoint {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
enabled: boolean;
|
||||||
|
tags: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClaudeCacheSettings {
|
export interface ClaudeCacheSettings {
|
||||||
@@ -23,76 +33,107 @@ export interface ClaudeCacheSettings {
|
|||||||
defaultSuffix: string;
|
defaultSuffix: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New: Tools-only config (cli-tools.json)
|
||||||
export interface ClaudeCliToolsConfig {
|
export interface ClaudeCliToolsConfig {
|
||||||
$schema?: string;
|
$schema?: string;
|
||||||
version: string;
|
version: string;
|
||||||
tools: Record<string, ClaudeCliTool>;
|
tools: Record<string, ClaudeCliTool>;
|
||||||
customEndpoints: Array<{
|
customEndpoints: ClaudeCustomEndpoint[];
|
||||||
id: string;
|
}
|
||||||
name: string;
|
|
||||||
enabled: boolean;
|
// New: Settings-only config (cli-settings.json)
|
||||||
}>;
|
export interface ClaudeCliSettingsConfig {
|
||||||
|
$schema?: string;
|
||||||
|
version: string;
|
||||||
defaultTool: string;
|
defaultTool: string;
|
||||||
settings: {
|
promptFormat: 'plain' | 'yaml' | 'json';
|
||||||
promptFormat: 'plain' | 'yaml' | 'json';
|
smartContext: {
|
||||||
smartContext: {
|
enabled: boolean;
|
||||||
enabled: boolean;
|
maxFiles: number;
|
||||||
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;
|
nativeResume?: boolean;
|
||||||
recursiveQuery: boolean;
|
recursiveQuery?: boolean;
|
||||||
cache: ClaudeCacheSettings;
|
cache?: Partial<ClaudeCacheSettings>;
|
||||||
codeIndexMcp: 'codexlens' | 'ace' | 'none'; // Code Index MCP provider
|
codeIndexMcp?: 'codexlens' | 'ace' | 'none';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Default Config ==========
|
// ========== Default Config ==========
|
||||||
|
|
||||||
const DEFAULT_CONFIG: ClaudeCliToolsConfig = {
|
const DEFAULT_TOOLS_CONFIG: ClaudeCliToolsConfig = {
|
||||||
version: '1.0.0',
|
version: '2.0.0',
|
||||||
tools: {
|
tools: {
|
||||||
gemini: {
|
gemini: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
isBuiltin: true,
|
isBuiltin: true,
|
||||||
command: 'gemini',
|
command: 'gemini',
|
||||||
description: 'Google AI for code analysis'
|
description: 'Google AI for code analysis',
|
||||||
|
tags: []
|
||||||
},
|
},
|
||||||
qwen: {
|
qwen: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
isBuiltin: true,
|
isBuiltin: true,
|
||||||
command: 'qwen',
|
command: 'qwen',
|
||||||
description: 'Alibaba AI assistant'
|
description: 'Alibaba AI assistant',
|
||||||
|
tags: []
|
||||||
},
|
},
|
||||||
codex: {
|
codex: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
isBuiltin: true,
|
isBuiltin: true,
|
||||||
command: 'codex',
|
command: 'codex',
|
||||||
description: 'OpenAI code generation'
|
description: 'OpenAI code generation',
|
||||||
|
tags: []
|
||||||
},
|
},
|
||||||
claude: {
|
claude: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
isBuiltin: true,
|
isBuiltin: true,
|
||||||
command: 'claude',
|
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',
|
defaultTool: 'gemini',
|
||||||
settings: {
|
promptFormat: 'plain',
|
||||||
promptFormat: 'plain',
|
smartContext: {
|
||||||
smartContext: {
|
enabled: false,
|
||||||
enabled: false,
|
maxFiles: 10
|
||||||
maxFiles: 10
|
},
|
||||||
},
|
nativeResume: true,
|
||||||
nativeResume: true,
|
recursiveQuery: true,
|
||||||
recursiveQuery: true,
|
cache: {
|
||||||
cache: {
|
injectionMode: 'auto',
|
||||||
injectionMode: 'auto',
|
defaultPrefix: '',
|
||||||
defaultPrefix: '',
|
defaultSuffix: ''
|
||||||
defaultSuffix: ''
|
},
|
||||||
},
|
codeIndexMcp: 'ace'
|
||||||
codeIndexMcp: 'codexlens' // Default to CodexLens
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== Helper Functions ==========
|
// ========== Helper Functions ==========
|
||||||
@@ -101,10 +142,18 @@ function getProjectConfigPath(projectDir: string): string {
|
|||||||
return path.join(projectDir, '.claude', 'cli-tools.json');
|
return path.join(projectDir, '.claude', 'cli-tools.json');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getProjectSettingsPath(projectDir: string): string {
|
||||||
|
return path.join(projectDir, '.claude', 'cli-settings.json');
|
||||||
|
}
|
||||||
|
|
||||||
function getGlobalConfigPath(): string {
|
function getGlobalConfigPath(): string {
|
||||||
return path.join(os.homedir(), '.claude', 'cli-tools.json');
|
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:
|
* Resolve config path with fallback:
|
||||||
* 1. Project: {projectDir}/.claude/cli-tools.json
|
* 1. Project: {projectDir}/.claude/cli-tools.json
|
||||||
@@ -125,6 +174,25 @@ function resolveConfigPath(projectDir: string): { path: string; source: 'project
|
|||||||
return { path: projectPath, source: 'default' };
|
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 {
|
function ensureClaudeDir(projectDir: string): void {
|
||||||
const claudeDir = path.join(projectDir, '.claude');
|
const claudeDir = path.join(projectDir, '.claude');
|
||||||
if (!fs.existsSync(claudeDir)) {
|
if (!fs.existsSync(claudeDir)) {
|
||||||
@@ -134,6 +202,20 @@ function ensureClaudeDir(projectDir: string): void {
|
|||||||
|
|
||||||
// ========== Main Functions ==========
|
// ========== 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:
|
* Load CLI tools configuration with fallback:
|
||||||
* 1. Project: {projectDir}/.claude/cli-tools.json
|
* 1. Project: {projectDir}/.claude/cli-tools.json
|
||||||
@@ -145,61 +227,115 @@ export function loadClaudeCliTools(projectDir: string): ClaudeCliToolsConfig & {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (resolved.source === 'default') {
|
if (resolved.source === 'default') {
|
||||||
// No config file found, return defaults
|
return { ...DEFAULT_TOOLS_CONFIG, _source: 'default' };
|
||||||
return { ...DEFAULT_CONFIG, _source: 'default' };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = fs.readFileSync(resolved.path, 'utf-8');
|
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
|
// Merge tools with defaults and ensure tags exist
|
||||||
const config = {
|
const mergedTools: Record<string, ClaudeCliTool> = {};
|
||||||
...DEFAULT_CONFIG,
|
for (const [key, tool] of Object.entries({ ...DEFAULT_TOOLS_CONFIG.tools, ...(parsed.tools || {}) })) {
|
||||||
...parsed,
|
mergedTools[key] = ensureToolTags(tool);
|
||||||
tools: { ...DEFAULT_CONFIG.tools, ...(parsed.tools || {}) },
|
}
|
||||||
settings: {
|
|
||||||
...DEFAULT_CONFIG.settings,
|
// Ensure customEndpoints have tags
|
||||||
...(parsed.settings || {}),
|
const mergedEndpoints = (parsed.customEndpoints || []).map(ep => ({
|
||||||
smartContext: {
|
...ep,
|
||||||
...DEFAULT_CONFIG.settings.smartContext,
|
tags: ep.tags ?? []
|
||||||
...(parsed.settings?.smartContext || {})
|
}));
|
||||||
},
|
|
||||||
cache: {
|
const config: ClaudeCliToolsConfig & { _source?: string } = {
|
||||||
...DEFAULT_CONFIG.settings.cache,
|
version: parsed.version || DEFAULT_TOOLS_CONFIG.version,
|
||||||
...(parsed.settings?.cache || {})
|
tools: mergedTools,
|
||||||
}
|
customEndpoints: mergedEndpoints,
|
||||||
},
|
$schema: parsed.$schema,
|
||||||
_source: resolved.source
|
_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;
|
return config;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[claude-cli-tools] Error loading config:', err);
|
console.error('[claude-cli-tools] Error loading tools config:', err);
|
||||||
return { ...DEFAULT_CONFIG, _source: 'default' };
|
return { ...DEFAULT_TOOLS_CONFIG, _source: 'default' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save CLI tools configuration to project .claude/cli-tools.json
|
* 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 {
|
export function saveClaudeCliTools(projectDir: string, config: ClaudeCliToolsConfig & { _source?: string }): void {
|
||||||
ensureClaudeDir(projectDir);
|
ensureClaudeDir(projectDir);
|
||||||
const configPath = getProjectConfigPath(projectDir);
|
const configPath = getProjectConfigPath(projectDir);
|
||||||
|
|
||||||
// Remove internal _source field before saving
|
|
||||||
const { _source, ...configToSave } = config;
|
const { _source, ...configToSave } = config;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
fs.writeFileSync(configPath, JSON.stringify(configToSave, null, 2), 'utf-8');
|
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) {
|
} 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}`);
|
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
|
* Update enabled status for a specific tool
|
||||||
*/
|
*/
|
||||||
@@ -224,16 +360,16 @@ export function updateClaudeToolEnabled(
|
|||||||
export function updateClaudeCacheSettings(
|
export function updateClaudeCacheSettings(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
cacheSettings: Partial<ClaudeCacheSettings>
|
cacheSettings: Partial<ClaudeCacheSettings>
|
||||||
): ClaudeCliToolsConfig {
|
): ClaudeCliSettingsConfig {
|
||||||
const config = loadClaudeCliTools(projectDir);
|
const settings = loadClaudeCliSettings(projectDir);
|
||||||
|
|
||||||
config.settings.cache = {
|
settings.cache = {
|
||||||
...config.settings.cache,
|
...settings.cache,
|
||||||
...cacheSettings
|
...cacheSettings
|
||||||
};
|
};
|
||||||
|
|
||||||
saveClaudeCliTools(projectDir, config);
|
saveClaudeCliSettings(projectDir, settings);
|
||||||
return config;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -242,11 +378,11 @@ export function updateClaudeCacheSettings(
|
|||||||
export function updateClaudeDefaultTool(
|
export function updateClaudeDefaultTool(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
defaultTool: string
|
defaultTool: string
|
||||||
): ClaudeCliToolsConfig {
|
): ClaudeCliSettingsConfig {
|
||||||
const config = loadClaudeCliTools(projectDir);
|
const settings = loadClaudeCliSettings(projectDir);
|
||||||
config.defaultTool = defaultTool;
|
settings.defaultTool = defaultTool;
|
||||||
saveClaudeCliTools(projectDir, config);
|
saveClaudeCliSettings(projectDir, settings);
|
||||||
return config;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -254,16 +390,23 @@ export function updateClaudeDefaultTool(
|
|||||||
*/
|
*/
|
||||||
export function addClaudeCustomEndpoint(
|
export function addClaudeCustomEndpoint(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
endpoint: { id: string; name: string; enabled: boolean }
|
endpoint: { id: string; name: string; enabled: boolean; tags?: string[] }
|
||||||
): ClaudeCliToolsConfig {
|
): ClaudeCliToolsConfig {
|
||||||
const config = loadClaudeCliTools(projectDir);
|
const config = loadClaudeCliTools(projectDir);
|
||||||
|
|
||||||
|
const newEndpoint: ClaudeCustomEndpoint = {
|
||||||
|
id: endpoint.id,
|
||||||
|
name: endpoint.name,
|
||||||
|
enabled: endpoint.enabled,
|
||||||
|
tags: endpoint.tags || []
|
||||||
|
};
|
||||||
|
|
||||||
// Check if endpoint already exists
|
// Check if endpoint already exists
|
||||||
const existingIndex = config.customEndpoints.findIndex(e => e.id === endpoint.id);
|
const existingIndex = config.customEndpoints.findIndex(e => e.id === endpoint.id);
|
||||||
if (existingIndex >= 0) {
|
if (existingIndex >= 0) {
|
||||||
config.customEndpoints[existingIndex] = endpoint;
|
config.customEndpoints[existingIndex] = newEndpoint;
|
||||||
} else {
|
} else {
|
||||||
config.customEndpoints.push(endpoint);
|
config.customEndpoints.push(newEndpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
saveClaudeCliTools(projectDir, config);
|
saveClaudeCliTools(projectDir, config);
|
||||||
@@ -309,12 +452,12 @@ export function getClaudeCliToolsInfo(projectDir: string): {
|
|||||||
export function updateCodeIndexMcp(
|
export function updateCodeIndexMcp(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
provider: 'codexlens' | 'ace' | 'none'
|
provider: 'codexlens' | 'ace' | 'none'
|
||||||
): { success: boolean; error?: string; config?: ClaudeCliToolsConfig } {
|
): { success: boolean; error?: string; settings?: ClaudeCliSettingsConfig } {
|
||||||
try {
|
try {
|
||||||
// Update config
|
// Update settings config
|
||||||
const config = loadClaudeCliTools(projectDir);
|
const settings = loadClaudeCliSettings(projectDir);
|
||||||
config.settings.codeIndexMcp = provider;
|
settings.codeIndexMcp = provider;
|
||||||
saveClaudeCliTools(projectDir, config);
|
saveClaudeCliSettings(projectDir, settings);
|
||||||
|
|
||||||
// Only update global CLAUDE.md (consistent with Chinese response / Windows platform)
|
// Only update global CLAUDE.md (consistent with Chinese response / Windows platform)
|
||||||
const globalClaudeMdPath = path.join(os.homedir(), '.claude', 'CLAUDE.md');
|
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}`);
|
console.log(`[claude-cli-tools] Updated global CLAUDE.md to use ${provider}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true, config };
|
return { success: true, settings };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[claude-cli-tools] Error updating Code Index MCP:', err);
|
console.error('[claude-cli-tools] Error updating Code Index MCP:', err);
|
||||||
return { success: false, error: (err as Error).message };
|
return { success: false, error: (err as Error).message };
|
||||||
@@ -369,8 +512,8 @@ export function updateCodeIndexMcp(
|
|||||||
* Get current Code Index MCP provider
|
* Get current Code Index MCP provider
|
||||||
*/
|
*/
|
||||||
export function getCodeIndexMcp(projectDir: string): 'codexlens' | 'ace' | 'none' {
|
export function getCodeIndexMcp(projectDir: string): 'codexlens' | 'ace' | 'none' {
|
||||||
const config = loadClaudeCliTools(projectDir);
|
const settings = loadClaudeCliSettings(projectDir);
|
||||||
return config.settings.codeIndexMcp || 'codexlens';
|
return settings.codeIndexMcp || 'ace';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { StoragePaths, ensureStorageDir } from '../config/storage-paths.js';
|
import { StoragePaths, ensureStorageDir } from '../config/storage-paths.js';
|
||||||
|
import { loadClaudeCliTools, saveClaudeCliTools } from './claude-cli-tools.js';
|
||||||
|
|
||||||
// ========== Types ==========
|
// ========== Types ==========
|
||||||
|
|
||||||
@@ -234,6 +235,20 @@ export function updateToolConfig(
|
|||||||
config.tools[tool] = updatedToolConfig;
|
config.tools[tool] = updatedToolConfig;
|
||||||
saveCliConfig(baseDir, config);
|
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;
|
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): {
|
export function getFullConfigResponse(baseDir: string): {
|
||||||
config: CliConfig;
|
config: CliConfig;
|
||||||
predefinedModels: Record<string, string[]>;
|
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 {
|
return {
|
||||||
config: loadCliConfig(baseDir),
|
config,
|
||||||
predefinedModels: { ...PREDEFINED_MODELS }
|
predefinedModels: { ...PREDEFINED_MODELS }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -353,6 +353,7 @@ export interface CodexLensEmbeddingRotation {
|
|||||||
/**
|
/**
|
||||||
* Generic embedding pool configuration (refactored from CodexLensEmbeddingRotation)
|
* Generic embedding pool configuration (refactored from CodexLensEmbeddingRotation)
|
||||||
* Supports automatic discovery of all providers offering a specific model
|
* Supports automatic discovery of all providers offering a specific model
|
||||||
|
* @deprecated Use ModelPoolConfig instead
|
||||||
*/
|
*/
|
||||||
export interface EmbeddingPoolConfig {
|
export interface EmbeddingPoolConfig {
|
||||||
/** Whether embedding pool is enabled */
|
/** Whether embedding pool is enabled */
|
||||||
@@ -377,6 +378,50 @@ export interface EmbeddingPoolConfig {
|
|||||||
defaultMaxConcurrentPerKey: number;
|
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
|
* Complete LiteLLM API configuration
|
||||||
* Root configuration object stored in JSON file
|
* Root configuration object stored in JSON file
|
||||||
@@ -400,6 +445,9 @@ export interface LiteLLMApiConfig {
|
|||||||
/** CodexLens multi-provider embedding rotation config (deprecated, use embeddingPoolConfig) */
|
/** CodexLens multi-provider embedding rotation config (deprecated, use embeddingPoolConfig) */
|
||||||
codexlensEmbeddingRotation?: CodexLensEmbeddingRotation;
|
codexlensEmbeddingRotation?: CodexLensEmbeddingRotation;
|
||||||
|
|
||||||
/** Generic embedding pool configuration with auto-discovery support */
|
/** Generic embedding pool configuration with auto-discovery support (deprecated, use modelPools) */
|
||||||
embeddingPoolConfig?: EmbeddingPoolConfig;
|
embeddingPoolConfig?: EmbeddingPoolConfig;
|
||||||
|
|
||||||
|
/** Multi-model pool configurations (supports embedding, LLM, reranker) */
|
||||||
|
modelPools?: ModelPoolConfig[];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user