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

@@ -0,0 +1,360 @@
/**
* LiteLLM API Configuration Manager
* Manages provider credentials, custom endpoints, and cache settings
*/
import { existsSync, readFileSync, writeFileSync } from 'fs';
import { join } from 'path';
import { StoragePaths, ensureStorageDir } from './storage-paths.js';
import type {
LiteLLMApiConfig,
ProviderCredential,
CustomEndpoint,
GlobalCacheSettings,
ProviderType,
CacheStrategy,
} from '../types/litellm-api-config.js';
/**
* Default configuration
*/
function getDefaultConfig(): LiteLLMApiConfig {
return {
version: 1,
providers: [],
endpoints: [],
globalCacheSettings: {
enabled: true,
cacheDir: '~/.ccw/cache/context',
maxTotalSizeMB: 100,
},
};
}
/**
* Get config file path for a project
*/
function getConfigPath(baseDir: string): string {
const paths = StoragePaths.project(baseDir);
ensureStorageDir(paths.config);
return join(paths.config, 'litellm-api-config.json');
}
/**
* Load configuration from file
*/
export function loadLiteLLMApiConfig(baseDir: string): LiteLLMApiConfig {
const configPath = getConfigPath(baseDir);
if (!existsSync(configPath)) {
return getDefaultConfig();
}
try {
const content = readFileSync(configPath, 'utf-8');
return JSON.parse(content) as LiteLLMApiConfig;
} catch (error) {
console.error('[LiteLLM Config] Failed to load config:', error);
return getDefaultConfig();
}
}
/**
* Save configuration to file
*/
function saveConfig(baseDir: string, config: LiteLLMApiConfig): void {
const configPath = getConfigPath(baseDir);
writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
}
/**
* Resolve environment variables in API key
* Supports ${ENV_VAR} syntax
*/
export function resolveEnvVar(value: string): string {
if (!value) return value;
const envVarMatch = value.match(/^\$\{(.+)\}$/);
if (envVarMatch) {
const envVarName = envVarMatch[1];
return process.env[envVarName] || '';
}
return value;
}
// ===========================
// Provider Management
// ===========================
/**
* Get all providers
*/
export function getAllProviders(baseDir: string): ProviderCredential[] {
const config = loadLiteLLMApiConfig(baseDir);
return config.providers;
}
/**
* Get provider by ID
*/
export function getProvider(baseDir: string, providerId: string): ProviderCredential | null {
const config = loadLiteLLMApiConfig(baseDir);
return config.providers.find((p) => p.id === providerId) || null;
}
/**
* Get provider with resolved environment variables
*/
export function getProviderWithResolvedEnvVars(
baseDir: string,
providerId: string
): (ProviderCredential & { resolvedApiKey: string }) | null {
const provider = getProvider(baseDir, providerId);
if (!provider) return null;
return {
...provider,
resolvedApiKey: resolveEnvVar(provider.apiKey),
};
}
/**
* Add new provider
*/
export function addProvider(
baseDir: string,
providerData: Omit<ProviderCredential, 'id' | 'createdAt' | 'updatedAt'>
): ProviderCredential {
const config = loadLiteLLMApiConfig(baseDir);
const provider: ProviderCredential = {
...providerData,
id: `${providerData.type}-${Date.now()}`,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
config.providers.push(provider);
saveConfig(baseDir, config);
return provider;
}
/**
* Update provider
*/
export function updateProvider(
baseDir: string,
providerId: string,
updates: Partial<Omit<ProviderCredential, 'id' | 'createdAt' | 'updatedAt'>>
): ProviderCredential {
const config = loadLiteLLMApiConfig(baseDir);
const providerIndex = config.providers.findIndex((p) => p.id === providerId);
if (providerIndex === -1) {
throw new Error(`Provider not found: ${providerId}`);
}
config.providers[providerIndex] = {
...config.providers[providerIndex],
...updates,
updatedAt: new Date().toISOString(),
};
saveConfig(baseDir, config);
return config.providers[providerIndex];
}
/**
* Delete provider
*/
export function deleteProvider(baseDir: string, providerId: string): boolean {
const config = loadLiteLLMApiConfig(baseDir);
const initialLength = config.providers.length;
config.providers = config.providers.filter((p) => p.id !== providerId);
if (config.providers.length === initialLength) {
return false;
}
// Also remove endpoints using this provider
config.endpoints = config.endpoints.filter((e) => e.providerId !== providerId);
saveConfig(baseDir, config);
return true;
}
// ===========================
// Endpoint Management
// ===========================
/**
* Get all endpoints
*/
export function getAllEndpoints(baseDir: string): CustomEndpoint[] {
const config = loadLiteLLMApiConfig(baseDir);
return config.endpoints;
}
/**
* Get endpoint by ID
*/
export function getEndpoint(baseDir: string, endpointId: string): CustomEndpoint | null {
const config = loadLiteLLMApiConfig(baseDir);
return config.endpoints.find((e) => e.id === endpointId) || null;
}
/**
* Find endpoint by ID (alias for getEndpoint)
*/
export function findEndpointById(baseDir: string, endpointId: string): CustomEndpoint | null {
return getEndpoint(baseDir, endpointId);
}
/**
* Add new endpoint
*/
export function addEndpoint(
baseDir: string,
endpointData: Omit<CustomEndpoint, 'createdAt' | 'updatedAt'>
): CustomEndpoint {
const config = loadLiteLLMApiConfig(baseDir);
// Check if ID already exists
if (config.endpoints.some((e) => e.id === endpointData.id)) {
throw new Error(`Endpoint ID already exists: ${endpointData.id}`);
}
// Verify provider exists
if (!config.providers.find((p) => p.id === endpointData.providerId)) {
throw new Error(`Provider not found: ${endpointData.providerId}`);
}
const endpoint: CustomEndpoint = {
...endpointData,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
config.endpoints.push(endpoint);
saveConfig(baseDir, config);
return endpoint;
}
/**
* Update endpoint
*/
export function updateEndpoint(
baseDir: string,
endpointId: string,
updates: Partial<Omit<CustomEndpoint, 'id' | 'createdAt' | 'updatedAt'>>
): CustomEndpoint {
const config = loadLiteLLMApiConfig(baseDir);
const endpointIndex = config.endpoints.findIndex((e) => e.id === endpointId);
if (endpointIndex === -1) {
throw new Error(`Endpoint not found: ${endpointId}`);
}
// Verify provider exists if updating providerId
if (updates.providerId && !config.providers.find((p) => p.id === updates.providerId)) {
throw new Error(`Provider not found: ${updates.providerId}`);
}
config.endpoints[endpointIndex] = {
...config.endpoints[endpointIndex],
...updates,
updatedAt: new Date().toISOString(),
};
saveConfig(baseDir, config);
return config.endpoints[endpointIndex];
}
/**
* Delete endpoint
*/
export function deleteEndpoint(baseDir: string, endpointId: string): boolean {
const config = loadLiteLLMApiConfig(baseDir);
const initialLength = config.endpoints.length;
config.endpoints = config.endpoints.filter((e) => e.id !== endpointId);
if (config.endpoints.length === initialLength) {
return false;
}
// Clear default endpoint if deleted
if (config.defaultEndpoint === endpointId) {
delete config.defaultEndpoint;
}
saveConfig(baseDir, config);
return true;
}
// ===========================
// Default Endpoint Management
// ===========================
/**
* Get default endpoint
*/
export function getDefaultEndpoint(baseDir: string): string | undefined {
const config = loadLiteLLMApiConfig(baseDir);
return config.defaultEndpoint;
}
/**
* Set default endpoint
*/
export function setDefaultEndpoint(baseDir: string, endpointId?: string): void {
const config = loadLiteLLMApiConfig(baseDir);
if (endpointId) {
// Verify endpoint exists
if (!config.endpoints.find((e) => e.id === endpointId)) {
throw new Error(`Endpoint not found: ${endpointId}`);
}
config.defaultEndpoint = endpointId;
} else {
delete config.defaultEndpoint;
}
saveConfig(baseDir, config);
}
// ===========================
// Cache Settings Management
// ===========================
/**
* Get global cache settings
*/
export function getGlobalCacheSettings(baseDir: string): GlobalCacheSettings {
const config = loadLiteLLMApiConfig(baseDir);
return config.globalCacheSettings;
}
/**
* Update global cache settings
*/
export function updateGlobalCacheSettings(
baseDir: string,
settings: Partial<GlobalCacheSettings>
): void {
const config = loadLiteLLMApiConfig(baseDir);
config.globalCacheSettings = {
...config.globalCacheSettings,
...settings,
};
saveConfig(baseDir, config);
}
// Re-export types
export type { ProviderCredential, CustomEndpoint, ProviderType, CacheStrategy };

View File

@@ -0,0 +1,259 @@
/**
* Provider Model Presets
*
* Predefined model information for each supported LLM provider.
* Used for UI dropdowns and validation.
*/
import type { ProviderType } from '../types/litellm-api-config.js';
/**
* Model information metadata
*/
export interface ModelInfo {
/** Model identifier (used in API calls) */
id: string;
/** Human-readable display name */
name: string;
/** Context window size in tokens */
contextWindow: number;
/** Whether this model supports prompt caching */
supportsCaching: boolean;
}
/**
* Predefined models for each provider
* Used for UI selection and validation
*/
export const PROVIDER_MODELS: Record<ProviderType, ModelInfo[]> = {
openai: [
{
id: 'gpt-4o',
name: 'GPT-4o',
contextWindow: 128000,
supportsCaching: true
},
{
id: 'gpt-4o-mini',
name: 'GPT-4o Mini',
contextWindow: 128000,
supportsCaching: true
},
{
id: 'o1',
name: 'O1',
contextWindow: 200000,
supportsCaching: true
},
{
id: 'o1-mini',
name: 'O1 Mini',
contextWindow: 128000,
supportsCaching: true
},
{
id: 'gpt-4-turbo',
name: 'GPT-4 Turbo',
contextWindow: 128000,
supportsCaching: false
}
],
anthropic: [
{
id: 'claude-sonnet-4-20250514',
name: 'Claude Sonnet 4',
contextWindow: 200000,
supportsCaching: true
},
{
id: 'claude-3-5-sonnet-20241022',
name: 'Claude 3.5 Sonnet',
contextWindow: 200000,
supportsCaching: true
},
{
id: 'claude-3-5-haiku-20241022',
name: 'Claude 3.5 Haiku',
contextWindow: 200000,
supportsCaching: true
},
{
id: 'claude-3-opus-20240229',
name: 'Claude 3 Opus',
contextWindow: 200000,
supportsCaching: false
}
],
ollama: [
{
id: 'llama3.2',
name: 'Llama 3.2',
contextWindow: 128000,
supportsCaching: false
},
{
id: 'llama3.1',
name: 'Llama 3.1',
contextWindow: 128000,
supportsCaching: false
},
{
id: 'qwen2.5-coder',
name: 'Qwen 2.5 Coder',
contextWindow: 32000,
supportsCaching: false
},
{
id: 'codellama',
name: 'Code Llama',
contextWindow: 16000,
supportsCaching: false
},
{
id: 'mistral',
name: 'Mistral',
contextWindow: 32000,
supportsCaching: false
}
],
azure: [
{
id: 'gpt-4o',
name: 'GPT-4o (Azure)',
contextWindow: 128000,
supportsCaching: true
},
{
id: 'gpt-4o-mini',
name: 'GPT-4o Mini (Azure)',
contextWindow: 128000,
supportsCaching: true
},
{
id: 'gpt-4-turbo',
name: 'GPT-4 Turbo (Azure)',
contextWindow: 128000,
supportsCaching: false
},
{
id: 'gpt-35-turbo',
name: 'GPT-3.5 Turbo (Azure)',
contextWindow: 16000,
supportsCaching: false
}
],
google: [
{
id: 'gemini-2.0-flash-exp',
name: 'Gemini 2.0 Flash Experimental',
contextWindow: 1048576,
supportsCaching: true
},
{
id: 'gemini-1.5-pro',
name: 'Gemini 1.5 Pro',
contextWindow: 2097152,
supportsCaching: true
},
{
id: 'gemini-1.5-flash',
name: 'Gemini 1.5 Flash',
contextWindow: 1048576,
supportsCaching: true
},
{
id: 'gemini-1.0-pro',
name: 'Gemini 1.0 Pro',
contextWindow: 32000,
supportsCaching: false
}
],
mistral: [
{
id: 'mistral-large-latest',
name: 'Mistral Large',
contextWindow: 128000,
supportsCaching: false
},
{
id: 'mistral-medium-latest',
name: 'Mistral Medium',
contextWindow: 32000,
supportsCaching: false
},
{
id: 'mistral-small-latest',
name: 'Mistral Small',
contextWindow: 32000,
supportsCaching: false
},
{
id: 'codestral-latest',
name: 'Codestral',
contextWindow: 32000,
supportsCaching: false
}
],
deepseek: [
{
id: 'deepseek-chat',
name: 'DeepSeek Chat',
contextWindow: 64000,
supportsCaching: false
},
{
id: 'deepseek-coder',
name: 'DeepSeek Coder',
contextWindow: 64000,
supportsCaching: false
}
],
custom: [
{
id: 'custom-model',
name: 'Custom Model',
contextWindow: 128000,
supportsCaching: false
}
]
};
/**
* Get models for a specific provider
* @param providerType - Provider type to get models for
* @returns Array of model information
*/
export function getModelsForProvider(providerType: ProviderType): ModelInfo[] {
return PROVIDER_MODELS[providerType] || [];
}
/**
* Get model information by ID within a provider
* @param providerType - Provider type
* @param modelId - Model identifier
* @returns Model information or undefined if not found
*/
export function getModelInfo(providerType: ProviderType, modelId: string): ModelInfo | undefined {
const models = PROVIDER_MODELS[providerType] || [];
return models.find(m => m.id === modelId);
}
/**
* Validate if a model ID is supported by a provider
* @param providerType - Provider type
* @param modelId - Model identifier to validate
* @returns true if model is valid for provider
*/
export function isValidModel(providerType: ProviderType, modelId: string): boolean {
return getModelInfo(providerType, modelId) !== undefined;
}