mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +08:00
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:
360
ccw/src/config/litellm-api-config-manager.ts
Normal file
360
ccw/src/config/litellm-api-config-manager.ts
Normal 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 };
|
||||
259
ccw/src/config/provider-models.ts
Normal file
259
ccw/src/config/provider-models.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user