feat: Add unified LiteLLM API management with dashboard UI and CLI integration

- Create ccw-litellm Python package with AbstractEmbedder and AbstractLLMClient interfaces
- Add BaseEmbedder abstraction and factory pattern to codex-lens for pluggable backends
- Implement API Settings dashboard page for provider credentials and custom endpoints
- Add REST API routes for CRUD operations on providers and endpoints
- Extend CLI with --model parameter for custom endpoint routing
- Integrate existing context-cache for @pattern file resolution
- Add provider model registry with predefined models per provider type
- Include i18n translations (en/zh) for all new UI elements

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
catlog22
2025-12-23 20:36:32 +08:00
parent 5228581324
commit bf66b095c7
44 changed files with 4948 additions and 19 deletions

View File

@@ -46,7 +46,8 @@ const MODULE_CSS_FILES = [
'27-graph-explorer.css',
'28-mcp-manager.css',
'29-help.css',
'30-core-memory.css'
'30-core-memory.css',
'31-api-settings.css'
];
const MODULE_FILES = [
@@ -95,6 +96,7 @@ const MODULE_FILES = [
'views/skills-manager.js',
'views/rules-manager.js',
'views/claude-manager.js',
'views/api-settings.js',
'views/help.js',
'main.js'
];

View File

@@ -0,0 +1,485 @@
// @ts-nocheck
/**
* LiteLLM API Routes Module
* Handles LiteLLM provider management, endpoint configuration, and cache management
*/
import type { IncomingMessage, ServerResponse } from 'http';
import {
getAllProviders,
getProvider,
addProvider,
updateProvider,
deleteProvider,
getAllEndpoints,
getEndpoint,
addEndpoint,
updateEndpoint,
deleteEndpoint,
getDefaultEndpoint,
setDefaultEndpoint,
getGlobalCacheSettings,
updateGlobalCacheSettings,
loadLiteLLMApiConfig,
type ProviderCredential,
type CustomEndpoint,
type ProviderType,
} from '../../config/litellm-api-config-manager.js';
import { getContextCacheStore } from '../../tools/context-cache-store.js';
import { getLiteLLMClient } from '../../tools/litellm-client.js';
export interface RouteContext {
pathname: string;
url: URL;
req: IncomingMessage;
res: ServerResponse;
initialPath: string;
handlePostRequest: (req: IncomingMessage, res: ServerResponse, handler: (body: unknown) => Promise<any>) => void;
broadcastToClients: (data: unknown) => void;
}
// ===========================
// Model Information
// ===========================
interface ModelInfo {
id: string;
name: string;
provider: ProviderType;
description?: string;
}
const PROVIDER_MODELS: Record<ProviderType, ModelInfo[]> = {
openai: [
{ id: 'gpt-4-turbo', name: 'GPT-4 Turbo', provider: 'openai', description: '128K context' },
{ id: 'gpt-4', name: 'GPT-4', provider: 'openai', description: '8K context' },
{ id: 'gpt-3.5-turbo', name: 'GPT-3.5 Turbo', provider: 'openai', description: '16K context' },
],
anthropic: [
{ id: 'claude-3-opus-20240229', name: 'Claude 3 Opus', provider: 'anthropic', description: '200K context' },
{ id: 'claude-3-sonnet-20240229', name: 'Claude 3 Sonnet', provider: 'anthropic', description: '200K context' },
{ id: 'claude-3-haiku-20240307', name: 'Claude 3 Haiku', provider: 'anthropic', description: '200K context' },
],
google: [
{ id: 'gemini-pro', name: 'Gemini Pro', provider: 'google', description: '32K context' },
{ id: 'gemini-pro-vision', name: 'Gemini Pro Vision', provider: 'google', description: '16K context' },
],
ollama: [
{ id: 'llama2', name: 'Llama 2', provider: 'ollama', description: 'Local model' },
{ id: 'mistral', name: 'Mistral', provider: 'ollama', description: 'Local model' },
],
azure: [],
mistral: [
{ id: 'mistral-large-latest', name: 'Mistral Large', provider: 'mistral', description: '32K context' },
{ id: 'mistral-medium-latest', name: 'Mistral Medium', provider: 'mistral', description: '32K context' },
],
deepseek: [
{ id: 'deepseek-chat', name: 'DeepSeek Chat', provider: 'deepseek', description: '64K context' },
{ id: 'deepseek-coder', name: 'DeepSeek Coder', provider: 'deepseek', description: '64K context' },
],
custom: [],
};
/**
* Handle LiteLLM API routes
* @returns true if route was handled, false otherwise
*/
export async function handleLiteLLMApiRoutes(ctx: RouteContext): Promise<boolean> {
const { pathname, url, req, res, initialPath, handlePostRequest, broadcastToClients } = ctx;
// ===========================
// Provider Management Routes
// ===========================
// GET /api/litellm-api/providers - List all providers
if (pathname === '/api/litellm-api/providers' && req.method === 'GET') {
try {
const providers = getAllProviders(initialPath);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ providers, count: providers.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/providers - Create provider
if (pathname === '/api/litellm-api/providers' && req.method === 'POST') {
handlePostRequest(req, res, async (body: unknown) => {
const providerData = body as Omit<ProviderCredential, 'id' | 'createdAt' | 'updatedAt'>;
if (!providerData.name || !providerData.type || !providerData.apiKey) {
return { error: 'Provider name, type, and apiKey are required', status: 400 };
}
try {
const provider = addProvider(initialPath, providerData);
broadcastToClients({
type: 'LITELLM_PROVIDER_CREATED',
payload: { provider, timestamp: new Date().toISOString() }
});
return { success: true, provider };
} catch (err) {
return { error: (err as Error).message, status: 500 };
}
});
return true;
}
// GET /api/litellm-api/providers/:id - Get provider by ID
const providerGetMatch = pathname.match(/^\/api\/litellm-api\/providers\/([^/]+)$/);
if (providerGetMatch && req.method === 'GET') {
const providerId = providerGetMatch[1];
try {
const provider = getProvider(initialPath, providerId);
if (!provider) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Provider not found' }));
return true;
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(provider));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (err as Error).message }));
}
return true;
}
// PUT /api/litellm-api/providers/:id - Update provider
const providerUpdateMatch = pathname.match(/^\/api\/litellm-api\/providers\/([^/]+)$/);
if (providerUpdateMatch && req.method === 'PUT') {
const providerId = providerUpdateMatch[1];
handlePostRequest(req, res, async (body: unknown) => {
const updates = body as Partial<Omit<ProviderCredential, 'id' | 'createdAt' | 'updatedAt'>>;
try {
const provider = updateProvider(initialPath, providerId, updates);
broadcastToClients({
type: 'LITELLM_PROVIDER_UPDATED',
payload: { provider, timestamp: new Date().toISOString() }
});
return { success: true, provider };
} catch (err) {
return { error: (err as Error).message, status: 404 };
}
});
return true;
}
// DELETE /api/litellm-api/providers/:id - Delete provider
const providerDeleteMatch = pathname.match(/^\/api\/litellm-api\/providers\/([^/]+)$/);
if (providerDeleteMatch && req.method === 'DELETE') {
const providerId = providerDeleteMatch[1];
try {
const success = deleteProvider(initialPath, providerId);
if (!success) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Provider not found' }));
return true;
}
broadcastToClients({
type: 'LITELLM_PROVIDER_DELETED',
payload: { providerId, timestamp: new Date().toISOString() }
});
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, message: 'Provider deleted' }));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (err as Error).message }));
}
return true;
}
// POST /api/litellm-api/providers/:id/test - Test provider connection
const providerTestMatch = pathname.match(/^\/api\/litellm-api\/providers\/([^/]+)\/test$/);
if (providerTestMatch && req.method === 'POST') {
const providerId = providerTestMatch[1];
try {
const provider = getProvider(initialPath, providerId);
if (!provider) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, error: 'Provider not found' }));
return true;
}
if (!provider.enabled) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, error: 'Provider is disabled' }));
return true;
}
// Test connection using litellm client
const client = getLiteLLMClient();
const available = await client.isAvailable();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: available, provider: provider.type }));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, error: (err as Error).message }));
}
return true;
}
// ===========================
// Endpoint Management Routes
// ===========================
// GET /api/litellm-api/endpoints - List all endpoints
if (pathname === '/api/litellm-api/endpoints' && req.method === 'GET') {
try {
const endpoints = getAllEndpoints(initialPath);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ endpoints, count: endpoints.length }));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (err as Error).message }));
}
return true;
}
// POST /api/litellm-api/endpoints - Create endpoint
if (pathname === '/api/litellm-api/endpoints' && req.method === 'POST') {
handlePostRequest(req, res, async (body: unknown) => {
const endpointData = body as Omit<CustomEndpoint, 'createdAt' | 'updatedAt'>;
if (!endpointData.id || !endpointData.name || !endpointData.providerId || !endpointData.model) {
return { error: 'Endpoint id, name, providerId, and model are required', status: 400 };
}
try {
const endpoint = addEndpoint(initialPath, endpointData);
broadcastToClients({
type: 'LITELLM_ENDPOINT_CREATED',
payload: { endpoint, timestamp: new Date().toISOString() }
});
return { success: true, endpoint };
} catch (err) {
return { error: (err as Error).message, status: 500 };
}
});
return true;
}
// GET /api/litellm-api/endpoints/:id - Get endpoint by ID
const endpointGetMatch = pathname.match(/^\/api\/litellm-api\/endpoints\/([^/]+)$/);
if (endpointGetMatch && req.method === 'GET') {
const endpointId = endpointGetMatch[1];
try {
const endpoint = getEndpoint(initialPath, endpointId);
if (!endpoint) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Endpoint not found' }));
return true;
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(endpoint));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (err as Error).message }));
}
return true;
}
// PUT /api/litellm-api/endpoints/:id - Update endpoint
const endpointUpdateMatch = pathname.match(/^\/api\/litellm-api\/endpoints\/([^/]+)$/);
if (endpointUpdateMatch && req.method === 'PUT') {
const endpointId = endpointUpdateMatch[1];
handlePostRequest(req, res, async (body: unknown) => {
const updates = body as Partial<Omit<CustomEndpoint, 'id' | 'createdAt' | 'updatedAt'>>;
try {
const endpoint = updateEndpoint(initialPath, endpointId, updates);
broadcastToClients({
type: 'LITELLM_ENDPOINT_UPDATED',
payload: { endpoint, timestamp: new Date().toISOString() }
});
return { success: true, endpoint };
} catch (err) {
return { error: (err as Error).message, status: 404 };
}
});
return true;
}
// DELETE /api/litellm-api/endpoints/:id - Delete endpoint
const endpointDeleteMatch = pathname.match(/^\/api\/litellm-api\/endpoints\/([^/]+)$/);
if (endpointDeleteMatch && req.method === 'DELETE') {
const endpointId = endpointDeleteMatch[1];
try {
const success = deleteEndpoint(initialPath, endpointId);
if (!success) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Endpoint not found' }));
return true;
}
broadcastToClients({
type: 'LITELLM_ENDPOINT_DELETED',
payload: { endpointId, timestamp: new Date().toISOString() }
});
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, message: 'Endpoint deleted' }));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (err as Error).message }));
}
return true;
}
// ===========================
// Model Discovery Routes
// ===========================
// GET /api/litellm-api/models/:providerType - Get available models for provider type
const modelsMatch = pathname.match(/^\/api\/litellm-api\/models\/([^/]+)$/);
if (modelsMatch && req.method === 'GET') {
const providerType = modelsMatch[1] as ProviderType;
try {
const models = PROVIDER_MODELS[providerType];
if (!models) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Provider type not found' }));
return true;
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ providerType, models, count: models.length }));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (err as Error).message }));
}
return true;
}
// ===========================
// Cache Management Routes
// ===========================
// GET /api/litellm-api/cache/stats - Get cache statistics
if (pathname === '/api/litellm-api/cache/stats' && req.method === 'GET') {
try {
const cacheStore = getContextCacheStore();
const stats = cacheStore.getStatus();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(stats));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (err as Error).message }));
}
return true;
}
// POST /api/litellm-api/cache/clear - Clear cache
if (pathname === '/api/litellm-api/cache/clear' && req.method === 'POST') {
try {
const cacheStore = getContextCacheStore();
const result = cacheStore.clear();
broadcastToClients({
type: 'LITELLM_CACHE_CLEARED',
payload: { removed: result.removed, timestamp: new Date().toISOString() }
});
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, removed: result.removed }));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (err as Error).message }));
}
return true;
}
// ===========================
// Config Management Routes
// ===========================
// GET /api/litellm-api/config - Get full config
if (pathname === '/api/litellm-api/config' && req.method === 'GET') {
try {
const config = loadLiteLLMApiConfig(initialPath);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(config));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (err as Error).message }));
}
return true;
}
// PUT /api/litellm-api/config/cache - Update global cache settings
if (pathname === '/api/litellm-api/config/cache' && req.method === 'PUT') {
handlePostRequest(req, res, async (body: unknown) => {
const settings = body as Partial<{ enabled: boolean; cacheDir: string; maxTotalSizeMB: number }>;
try {
updateGlobalCacheSettings(initialPath, settings);
const updatedSettings = getGlobalCacheSettings(initialPath);
broadcastToClients({
type: 'LITELLM_CACHE_SETTINGS_UPDATED',
payload: { settings: updatedSettings, timestamp: new Date().toISOString() }
});
return { success: true, settings: updatedSettings };
} catch (err) {
return { error: (err as Error).message, status: 500 };
}
});
return true;
}
// PUT /api/litellm-api/config/default-endpoint - Set default endpoint
if (pathname === '/api/litellm-api/config/default-endpoint' && req.method === 'PUT') {
handlePostRequest(req, res, async (body: unknown) => {
const { endpointId } = body as { endpointId?: string };
try {
setDefaultEndpoint(initialPath, endpointId);
const defaultEndpoint = getDefaultEndpoint(initialPath);
broadcastToClients({
type: 'LITELLM_DEFAULT_ENDPOINT_UPDATED',
payload: { endpointId, defaultEndpoint, timestamp: new Date().toISOString() }
});
return { success: true, defaultEndpoint };
} catch (err) {
return { error: (err as Error).message, status: 500 };
}
});
return true;
}
return false;
}

View File

@@ -0,0 +1,107 @@
// @ts-nocheck
/**
* LiteLLM Routes Module
* Handles all LiteLLM-related API endpoints
*/
import type { IncomingMessage, ServerResponse } from 'http';
import { getLiteLLMClient, getLiteLLMStatus, checkLiteLLMAvailable } from '../../tools/litellm-client.js';
export interface RouteContext {
pathname: string;
url: URL;
req: IncomingMessage;
res: ServerResponse;
initialPath: string;
handlePostRequest: (req: IncomingMessage, res: ServerResponse, handler: (body: unknown) => Promise<any>) => void;
broadcastToClients: (data: unknown) => void;
}
/**
* Handle LiteLLM routes
* @returns true if route was handled, false otherwise
*/
export async function handleLiteLLMRoutes(ctx: RouteContext): Promise<boolean> {
const { pathname, url, req, res, initialPath, handlePostRequest } = ctx;
// API: LiteLLM Status - Check availability and version
if (pathname === '/api/litellm/status') {
try {
const status = await getLiteLLMStatus();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(status));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ available: false, error: err.message }));
}
return true;
}
// API: LiteLLM Config - Get configuration
if (pathname === '/api/litellm/config' && req.method === 'GET') {
try {
const client = getLiteLLMClient();
const config = await client.getConfig();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(config));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: err.message }));
}
return true;
}
// API: LiteLLM Embed - Generate embeddings
if (pathname === '/api/litellm/embed' && req.method === 'POST') {
handlePostRequest(req, res, async (body) => {
const { texts, model = 'default' } = body;
if (!texts || !Array.isArray(texts)) {
return { error: 'texts array is required', status: 400 };
}
if (texts.length === 0) {
return { error: 'texts array cannot be empty', status: 400 };
}
try {
const client = getLiteLLMClient();
const result = await client.embed(texts, model);
return { success: true, ...result };
} catch (err) {
return { error: err.message, status: 500 };
}
});
return true;
}
// API: LiteLLM Chat - Chat with LLM
if (pathname === '/api/litellm/chat' && req.method === 'POST') {
handlePostRequest(req, res, async (body) => {
const { message, messages, model = 'default' } = body;
// Support both single message and messages array
if (!message && (!messages || !Array.isArray(messages))) {
return { error: 'message or messages array is required', status: 400 };
}
try {
const client = getLiteLLMClient();
if (messages && Array.isArray(messages)) {
// Multi-turn chat
const result = await client.chatMessages(messages, model);
return { success: true, ...result };
} else {
// Single message chat
const content = await client.chat(message, model);
return { success: true, content, model };
}
} catch (err) {
return { error: err.message, status: 500 };
}
});
return true;
}
return false;
}

View File

@@ -22,6 +22,8 @@ import { handleSessionRoutes } from './routes/session-routes.js';
import { handleCcwRoutes } from './routes/ccw-routes.js';
import { handleClaudeRoutes } from './routes/claude-routes.js';
import { handleHelpRoutes } from './routes/help-routes.js';
import { handleLiteLLMRoutes } from './routes/litellm-routes.js';
import { handleLiteLLMApiRoutes } from './routes/litellm-api-routes.js';
// Import WebSocket handling
import { handleWebSocketUpgrade, broadcastToClients } from './websocket.js';
@@ -311,6 +313,16 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
if (await handleCodexLensRoutes(routeContext)) return;
}
// LiteLLM routes (/api/litellm/*)
if (pathname.startsWith('/api/litellm/')) {
if (await handleLiteLLMRoutes(routeContext)) return;
}
// LiteLLM API routes (/api/litellm-api/*)
if (pathname.startsWith('/api/litellm-api/')) {
if (await handleLiteLLMApiRoutes(routeContext)) return;
}
// Graph routes (/api/graph/*)
if (pathname.startsWith('/api/graph/')) {
if (await handleGraphRoutes(routeContext)) return;