claude-code-workflow 6.2.7 → 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/CLAUDE.md +16 -1
- package/.claude/workflows/cli-templates/protocols/analysis-protocol.md +11 -4
- package/.claude/workflows/cli-templates/protocols/write-protocol.md +10 -75
- package/.claude/workflows/cli-tools-usage.md +14 -24
- package/.codex/AGENTS.md +51 -1
- package/.codex/prompts/compact.md +378 -0
- package/.gemini/GEMINI.md +57 -20
- package/ccw/dist/cli.d.ts.map +1 -1
- package/ccw/dist/cli.js +21 -8
- package/ccw/dist/cli.js.map +1 -1
- package/ccw/dist/commands/cli.d.ts +2 -0
- package/ccw/dist/commands/cli.d.ts.map +1 -1
- package/ccw/dist/commands/cli.js +129 -8
- package/ccw/dist/commands/cli.js.map +1 -1
- package/ccw/dist/commands/hook.d.ts.map +1 -1
- package/ccw/dist/commands/hook.js +3 -2
- package/ccw/dist/commands/hook.js.map +1 -1
- package/ccw/dist/config/litellm-api-config-manager.d.ts +180 -0
- package/ccw/dist/config/litellm-api-config-manager.d.ts.map +1 -0
- package/ccw/dist/config/litellm-api-config-manager.js +770 -0
- package/ccw/dist/config/litellm-api-config-manager.js.map +1 -0
- package/ccw/dist/config/provider-models.d.ts +73 -0
- package/ccw/dist/config/provider-models.d.ts.map +1 -0
- package/ccw/dist/config/provider-models.js +172 -0
- package/ccw/dist/config/provider-models.js.map +1 -0
- package/ccw/dist/core/cache-manager.d.ts.map +1 -1
- package/ccw/dist/core/cache-manager.js +3 -5
- package/ccw/dist/core/cache-manager.js.map +1 -1
- package/ccw/dist/core/dashboard-generator.d.ts.map +1 -1
- package/ccw/dist/core/dashboard-generator.js +3 -1
- package/ccw/dist/core/dashboard-generator.js.map +1 -1
- package/ccw/dist/core/routes/cli-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/cli-routes.js +169 -0
- package/ccw/dist/core/routes/cli-routes.js.map +1 -1
- package/ccw/dist/core/routes/codexlens-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/codexlens-routes.js +234 -18
- package/ccw/dist/core/routes/codexlens-routes.js.map +1 -1
- package/ccw/dist/core/routes/hooks-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/hooks-routes.js +30 -32
- package/ccw/dist/core/routes/hooks-routes.js.map +1 -1
- package/ccw/dist/core/routes/litellm-api-routes.d.ts +21 -0
- package/ccw/dist/core/routes/litellm-api-routes.d.ts.map +1 -0
- package/ccw/dist/core/routes/litellm-api-routes.js +780 -0
- package/ccw/dist/core/routes/litellm-api-routes.js.map +1 -0
- package/ccw/dist/core/routes/litellm-routes.d.ts +20 -0
- package/ccw/dist/core/routes/litellm-routes.d.ts.map +1 -0
- package/ccw/dist/core/routes/litellm-routes.js +85 -0
- package/ccw/dist/core/routes/litellm-routes.js.map +1 -0
- package/ccw/dist/core/routes/mcp-routes.js +2 -2
- package/ccw/dist/core/routes/mcp-routes.js.map +1 -1
- package/ccw/dist/core/routes/status-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/status-routes.js +39 -0
- package/ccw/dist/core/routes/status-routes.js.map +1 -1
- package/ccw/dist/core/routes/system-routes.js +1 -1
- package/ccw/dist/core/routes/system-routes.js.map +1 -1
- package/ccw/dist/core/server.d.ts.map +1 -1
- package/ccw/dist/core/server.js +15 -1
- package/ccw/dist/core/server.js.map +1 -1
- package/ccw/dist/mcp-server/index.js +1 -1
- package/ccw/dist/mcp-server/index.js.map +1 -1
- package/ccw/dist/tools/claude-cli-tools.d.ts +82 -0
- package/ccw/dist/tools/claude-cli-tools.d.ts.map +1 -0
- package/ccw/dist/tools/claude-cli-tools.js +216 -0
- package/ccw/dist/tools/claude-cli-tools.js.map +1 -0
- package/ccw/dist/tools/cli-executor.d.ts.map +1 -1
- package/ccw/dist/tools/cli-executor.js +76 -14
- package/ccw/dist/tools/cli-executor.js.map +1 -1
- package/ccw/dist/tools/codex-lens.d.ts +9 -2
- package/ccw/dist/tools/codex-lens.d.ts.map +1 -1
- package/ccw/dist/tools/codex-lens.js +114 -9
- package/ccw/dist/tools/codex-lens.js.map +1 -1
- package/ccw/dist/tools/context-cache-store.d.ts +136 -0
- package/ccw/dist/tools/context-cache-store.d.ts.map +1 -0
- package/ccw/dist/tools/context-cache-store.js +256 -0
- package/ccw/dist/tools/context-cache-store.js.map +1 -0
- package/ccw/dist/tools/context-cache.d.ts +56 -0
- package/ccw/dist/tools/context-cache.d.ts.map +1 -0
- package/ccw/dist/tools/context-cache.js +294 -0
- package/ccw/dist/tools/context-cache.js.map +1 -0
- package/ccw/dist/tools/core-memory.d.ts.map +1 -1
- package/ccw/dist/tools/core-memory.js +33 -19
- package/ccw/dist/tools/core-memory.js.map +1 -1
- package/ccw/dist/tools/index.d.ts.map +1 -1
- package/ccw/dist/tools/index.js +2 -0
- package/ccw/dist/tools/index.js.map +1 -1
- package/ccw/dist/tools/litellm-client.d.ts +85 -0
- package/ccw/dist/tools/litellm-client.d.ts.map +1 -0
- package/ccw/dist/tools/litellm-client.js +188 -0
- package/ccw/dist/tools/litellm-client.js.map +1 -0
- package/ccw/dist/tools/litellm-executor.d.ts +34 -0
- package/ccw/dist/tools/litellm-executor.d.ts.map +1 -0
- package/ccw/dist/tools/litellm-executor.js +192 -0
- package/ccw/dist/tools/litellm-executor.js.map +1 -0
- package/ccw/dist/tools/pattern-parser.d.ts +55 -0
- package/ccw/dist/tools/pattern-parser.d.ts.map +1 -0
- package/ccw/dist/tools/pattern-parser.js +237 -0
- package/ccw/dist/tools/pattern-parser.js.map +1 -0
- package/ccw/dist/tools/smart-search.d.ts +1 -0
- package/ccw/dist/tools/smart-search.d.ts.map +1 -1
- package/ccw/dist/tools/smart-search.js +117 -41
- package/ccw/dist/tools/smart-search.js.map +1 -1
- package/ccw/dist/types/litellm-api-config.d.ts +294 -0
- package/ccw/dist/types/litellm-api-config.d.ts.map +1 -0
- package/ccw/dist/types/litellm-api-config.js +8 -0
- package/ccw/dist/types/litellm-api-config.js.map +1 -0
- package/ccw/src/cli.ts +258 -244
- package/ccw/src/commands/cli.ts +153 -9
- package/ccw/src/commands/hook.ts +3 -2
- package/ccw/src/config/.litellm-api-config-manager.ts.2025-12-23T11-57-43-727Z.bak +441 -0
- package/ccw/src/config/litellm-api-config-manager.ts +1012 -0
- package/ccw/src/config/provider-models.ts +222 -0
- package/ccw/src/core/cache-manager.ts +292 -294
- package/ccw/src/core/dashboard-generator.ts +3 -1
- package/ccw/src/core/routes/cli-routes.ts +192 -0
- package/ccw/src/core/routes/codexlens-routes.ts +241 -19
- package/ccw/src/core/routes/hooks-routes.ts +399 -405
- package/ccw/src/core/routes/litellm-api-routes.ts +930 -0
- package/ccw/src/core/routes/litellm-routes.ts +107 -0
- package/ccw/src/core/routes/mcp-routes.ts +1271 -1271
- package/ccw/src/core/routes/status-routes.ts +51 -0
- package/ccw/src/core/routes/system-routes.ts +1 -1
- package/ccw/src/core/server.ts +15 -1
- package/ccw/src/mcp-server/index.ts +1 -1
- package/ccw/src/templates/dashboard-css/12-cli-legacy.css +44 -0
- package/ccw/src/templates/dashboard-css/31-api-settings.css +2265 -0
- package/ccw/src/templates/dashboard-js/components/cli-history.js +15 -8
- package/ccw/src/templates/dashboard-js/components/cli-status.js +323 -9
- package/ccw/src/templates/dashboard-js/components/navigation.js +329 -313
- package/ccw/src/templates/dashboard-js/i18n.js +583 -1
- package/ccw/src/templates/dashboard-js/views/api-settings.js +3362 -0
- package/ccw/src/templates/dashboard-js/views/cli-manager.js +199 -24
- package/ccw/src/templates/dashboard-js/views/codexlens-manager.js +1265 -27
- package/ccw/src/templates/dashboard.html +840 -831
- package/ccw/src/tools/claude-cli-tools.ts +300 -0
- package/ccw/src/tools/cli-executor.ts +83 -14
- package/ccw/src/tools/codex-lens.ts +146 -9
- package/ccw/src/tools/context-cache-store.ts +368 -0
- package/ccw/src/tools/context-cache.ts +393 -0
- package/ccw/src/tools/core-memory.ts +33 -19
- package/ccw/src/tools/index.ts +2 -0
- package/ccw/src/tools/litellm-client.ts +246 -0
- package/ccw/src/tools/litellm-executor.ts +241 -0
- package/ccw/src/tools/pattern-parser.ts +329 -0
- package/ccw/src/tools/smart-search.ts +142 -41
- package/ccw/src/types/litellm-api-config.ts +402 -0
- package/ccw-litellm/README.md +180 -0
- package/ccw-litellm/pyproject.toml +35 -0
- package/ccw-litellm/src/ccw_litellm/__init__.py +47 -0
- package/ccw-litellm/src/ccw_litellm/__pycache__/__init__.cpython-313.pyc +0 -0
- package/ccw-litellm/src/ccw_litellm/__pycache__/cli.cpython-313.pyc +0 -0
- package/ccw-litellm/src/ccw_litellm/cli.py +108 -0
- package/ccw-litellm/src/ccw_litellm/clients/__init__.py +12 -0
- package/ccw-litellm/src/ccw_litellm/clients/__pycache__/__init__.cpython-313.pyc +0 -0
- package/ccw-litellm/src/ccw_litellm/clients/__pycache__/litellm_embedder.cpython-313.pyc +0 -0
- package/ccw-litellm/src/ccw_litellm/clients/__pycache__/litellm_llm.cpython-313.pyc +0 -0
- package/ccw-litellm/src/ccw_litellm/clients/litellm_embedder.py +251 -0
- package/ccw-litellm/src/ccw_litellm/clients/litellm_llm.py +165 -0
- package/ccw-litellm/src/ccw_litellm/config/__init__.py +22 -0
- package/ccw-litellm/src/ccw_litellm/config/__pycache__/__init__.cpython-313.pyc +0 -0
- package/ccw-litellm/src/ccw_litellm/config/__pycache__/loader.cpython-313.pyc +0 -0
- package/ccw-litellm/src/ccw_litellm/config/__pycache__/models.cpython-313.pyc +0 -0
- package/ccw-litellm/src/ccw_litellm/config/loader.py +316 -0
- package/ccw-litellm/src/ccw_litellm/config/models.py +130 -0
- package/ccw-litellm/src/ccw_litellm/interfaces/__init__.py +14 -0
- package/ccw-litellm/src/ccw_litellm/interfaces/__pycache__/__init__.cpython-313.pyc +0 -0
- package/ccw-litellm/src/ccw_litellm/interfaces/__pycache__/embedder.cpython-313.pyc +0 -0
- package/ccw-litellm/src/ccw_litellm/interfaces/__pycache__/llm.cpython-313.pyc +0 -0
- package/ccw-litellm/src/ccw_litellm/interfaces/embedder.py +52 -0
- package/ccw-litellm/src/ccw_litellm/interfaces/llm.py +45 -0
- package/codex-lens/src/codexlens/__pycache__/config.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/cli/__pycache__/commands.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/cli/__pycache__/embedding_manager.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/cli/__pycache__/model_manager.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/cli/__pycache__/output.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/cli/commands.py +378 -23
- package/codex-lens/src/codexlens/cli/embedding_manager.py +660 -56
- package/codex-lens/src/codexlens/cli/model_manager.py +31 -18
- package/codex-lens/src/codexlens/cli/output.py +12 -1
- package/codex-lens/src/codexlens/config.py +93 -0
- package/codex-lens/src/codexlens/search/__pycache__/chain_search.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/search/__pycache__/hybrid_search.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/search/__pycache__/ranking.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/search/chain_search.py +6 -2
- package/codex-lens/src/codexlens/search/hybrid_search.py +44 -21
- package/codex-lens/src/codexlens/search/ranking.py +1 -1
- package/codex-lens/src/codexlens/semantic/__init__.py +42 -0
- package/codex-lens/src/codexlens/semantic/__pycache__/__init__.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/semantic/__pycache__/base.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/semantic/__pycache__/chunker.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/semantic/__pycache__/embedder.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/semantic/__pycache__/factory.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/semantic/__pycache__/gpu_support.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/semantic/__pycache__/litellm_embedder.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/semantic/__pycache__/vector_store.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/semantic/base.py +61 -0
- package/codex-lens/src/codexlens/semantic/chunker.py +43 -20
- package/codex-lens/src/codexlens/semantic/embedder.py +60 -13
- package/codex-lens/src/codexlens/semantic/factory.py +98 -0
- package/codex-lens/src/codexlens/semantic/gpu_support.py +225 -3
- package/codex-lens/src/codexlens/semantic/litellm_embedder.py +144 -0
- package/codex-lens/src/codexlens/semantic/rotational_embedder.py +434 -0
- package/codex-lens/src/codexlens/semantic/vector_store.py +33 -8
- package/codex-lens/src/codexlens/storage/__pycache__/path_mapper.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/storage/migrations/__pycache__/migration_004_dual_fts.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/storage/path_mapper.py +27 -1
- package/package.json +15 -5
- package/.codex/prompts.zip +0 -0
- package/ccw/package.json +0 -65
|
@@ -0,0 +1,3362 @@
|
|
|
1
|
+
// API Settings View
|
|
2
|
+
// Manages LiteLLM API providers, custom endpoints, and cache settings
|
|
3
|
+
|
|
4
|
+
// ========== State Management ==========
|
|
5
|
+
let apiSettingsData = null;
|
|
6
|
+
const providerModels = {};
|
|
7
|
+
let currentModal = null;
|
|
8
|
+
|
|
9
|
+
// New state for split layout
|
|
10
|
+
let selectedProviderId = null;
|
|
11
|
+
let providerSearchQuery = '';
|
|
12
|
+
let activeModelTab = 'llm';
|
|
13
|
+
let expandedModelGroups = new Set();
|
|
14
|
+
let activeSidebarTab = 'providers'; // 'providers' | 'endpoints' | 'cache' | 'embedding-pool'
|
|
15
|
+
|
|
16
|
+
// Embedding Pool state
|
|
17
|
+
let embeddingPoolConfig = null;
|
|
18
|
+
let embeddingPoolAvailableModels = [];
|
|
19
|
+
let embeddingPoolDiscoveredProviders = [];
|
|
20
|
+
|
|
21
|
+
// Cache for ccw-litellm status (frontend cache with TTL)
|
|
22
|
+
let ccwLitellmStatusCache = null;
|
|
23
|
+
let ccwLitellmStatusCacheTime = 0;
|
|
24
|
+
const CCW_LITELLM_STATUS_CACHE_TTL = 60000; // 60 seconds
|
|
25
|
+
|
|
26
|
+
// Track if this is the first render (force refresh on first load)
|
|
27
|
+
let isFirstApiSettingsRender = true;
|
|
28
|
+
|
|
29
|
+
// ========== Data Loading ==========
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Load API configuration
|
|
33
|
+
* @param {boolean} forceRefresh - Force refresh from server, bypass cache
|
|
34
|
+
*/
|
|
35
|
+
async function loadApiSettings(forceRefresh = false) {
|
|
36
|
+
// If not forcing refresh and data already exists, return cached data
|
|
37
|
+
if (!forceRefresh && apiSettingsData && apiSettingsData.providers) {
|
|
38
|
+
console.log('[API Settings] Using cached API settings data');
|
|
39
|
+
return apiSettingsData;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
console.log('[API Settings] Fetching API settings from server...');
|
|
44
|
+
const response = await fetch('/api/litellm-api/config');
|
|
45
|
+
if (!response.ok) throw new Error('Failed to load API settings');
|
|
46
|
+
apiSettingsData = await response.json();
|
|
47
|
+
return apiSettingsData;
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error('Failed to load API settings:', err);
|
|
50
|
+
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Load available models for a provider type
|
|
57
|
+
*/
|
|
58
|
+
async function loadProviderModels(providerType) {
|
|
59
|
+
try {
|
|
60
|
+
const response = await fetch('/api/litellm-api/models/' + providerType);
|
|
61
|
+
if (!response.ok) throw new Error('Failed to load models');
|
|
62
|
+
const data = await response.json();
|
|
63
|
+
providerModels[providerType] = data.models || [];
|
|
64
|
+
return data.models;
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.error('Failed to load provider models:', err);
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Load cache statistics
|
|
73
|
+
*/
|
|
74
|
+
async function loadCacheStats() {
|
|
75
|
+
try {
|
|
76
|
+
const response = await fetch('/api/litellm-api/cache/stats');
|
|
77
|
+
if (!response.ok) throw new Error('Failed to load cache stats');
|
|
78
|
+
return await response.json();
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.error('Failed to load cache stats:', err);
|
|
81
|
+
return { enabled: false, totalSize: 0, maxSize: 104857600, entries: 0 };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Load embedding pool configuration and available models
|
|
87
|
+
*/
|
|
88
|
+
async function loadEmbeddingPoolConfig() {
|
|
89
|
+
try {
|
|
90
|
+
const response = await fetch('/api/litellm-api/embedding-pool');
|
|
91
|
+
if (!response.ok) throw new Error('Failed to load embedding pool config');
|
|
92
|
+
const data = await response.json();
|
|
93
|
+
embeddingPoolConfig = data.poolConfig;
|
|
94
|
+
embeddingPoolAvailableModels = data.availableModels || [];
|
|
95
|
+
|
|
96
|
+
// If pool is enabled and has a target model, discover providers
|
|
97
|
+
if (embeddingPoolConfig && embeddingPoolConfig.enabled && embeddingPoolConfig.targetModel) {
|
|
98
|
+
await discoverProvidersForTargetModel(embeddingPoolConfig.targetModel);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return data;
|
|
102
|
+
} catch (err) {
|
|
103
|
+
console.error('Failed to load embedding pool config:', err);
|
|
104
|
+
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Discover providers for a specific target model
|
|
111
|
+
*/
|
|
112
|
+
async function discoverProvidersForTargetModel(targetModel) {
|
|
113
|
+
try {
|
|
114
|
+
const response = await fetch('/api/litellm-api/embedding-pool/discover/' + encodeURIComponent(targetModel));
|
|
115
|
+
if (!response.ok) throw new Error('Failed to discover providers');
|
|
116
|
+
const data = await response.json();
|
|
117
|
+
embeddingPoolDiscoveredProviders = data.discovered || [];
|
|
118
|
+
return data;
|
|
119
|
+
} catch (err) {
|
|
120
|
+
console.error('Failed to discover providers:', err);
|
|
121
|
+
embeddingPoolDiscoveredProviders = [];
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Save embedding pool configuration
|
|
128
|
+
*/
|
|
129
|
+
async function saveEmbeddingPoolConfig() {
|
|
130
|
+
try {
|
|
131
|
+
const enabled = document.getElementById('embedding-pool-enabled')?.checked || false;
|
|
132
|
+
const targetModel = document.getElementById('embedding-pool-target-model')?.value || '';
|
|
133
|
+
const strategy = document.getElementById('embedding-pool-strategy')?.value || 'round_robin';
|
|
134
|
+
const defaultCooldown = parseInt(document.getElementById('embedding-pool-cooldown')?.value || '60');
|
|
135
|
+
const defaultMaxConcurrentPerKey = parseInt(document.getElementById('embedding-pool-concurrent')?.value || '4');
|
|
136
|
+
|
|
137
|
+
const poolConfig = enabled ? {
|
|
138
|
+
enabled: true,
|
|
139
|
+
targetModel: targetModel,
|
|
140
|
+
strategy: strategy,
|
|
141
|
+
autoDiscover: true,
|
|
142
|
+
excludedProviderIds: embeddingPoolConfig?.excludedProviderIds || [],
|
|
143
|
+
defaultCooldown: defaultCooldown,
|
|
144
|
+
defaultMaxConcurrentPerKey: defaultMaxConcurrentPerKey
|
|
145
|
+
} : null;
|
|
146
|
+
|
|
147
|
+
const response = await fetch('/api/litellm-api/embedding-pool', {
|
|
148
|
+
method: 'PUT',
|
|
149
|
+
headers: { 'Content-Type': 'application/json' },
|
|
150
|
+
body: JSON.stringify(poolConfig)
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (!response.ok) throw new Error('Failed to save embedding pool config');
|
|
154
|
+
|
|
155
|
+
const result = await response.json();
|
|
156
|
+
embeddingPoolConfig = result.poolConfig;
|
|
157
|
+
|
|
158
|
+
const syncCount = result.syncResult?.syncedEndpoints?.length || 0;
|
|
159
|
+
showRefreshToast(t('apiSettings.poolSaved') + (syncCount > 0 ? ' (' + syncCount + ' endpoints synced)' : ''), 'success');
|
|
160
|
+
|
|
161
|
+
// Invalidate API settings cache since endpoints may have been synced
|
|
162
|
+
apiSettingsData = null;
|
|
163
|
+
|
|
164
|
+
// Reload the embedding pool section
|
|
165
|
+
await renderEmbeddingPoolMainPanel();
|
|
166
|
+
|
|
167
|
+
// Update sidebar summary
|
|
168
|
+
const sidebarContainer = document.querySelector('.api-settings-sidebar');
|
|
169
|
+
if (sidebarContainer) {
|
|
170
|
+
const contentArea = sidebarContainer.querySelector('.provider-list, .endpoints-list, .embedding-pool-sidebar-info, .embedding-pool-sidebar-summary, .cache-sidebar-info');
|
|
171
|
+
if (contentArea && contentArea.parentElement) {
|
|
172
|
+
contentArea.parentElement.innerHTML = renderEmbeddingPoolSidebar();
|
|
173
|
+
if (window.lucide) lucide.createIcons();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
} catch (err) {
|
|
178
|
+
console.error('Failed to save embedding pool config:', err);
|
|
179
|
+
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Toggle provider exclusion in embedding pool
|
|
185
|
+
*/
|
|
186
|
+
async function toggleProviderExclusion(providerId) {
|
|
187
|
+
if (!embeddingPoolConfig) return;
|
|
188
|
+
|
|
189
|
+
const excludedIds = embeddingPoolConfig.excludedProviderIds || [];
|
|
190
|
+
const index = excludedIds.indexOf(providerId);
|
|
191
|
+
|
|
192
|
+
if (index > -1) {
|
|
193
|
+
excludedIds.splice(index, 1);
|
|
194
|
+
} else {
|
|
195
|
+
excludedIds.push(providerId);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
embeddingPoolConfig.excludedProviderIds = excludedIds;
|
|
199
|
+
|
|
200
|
+
// Re-render the discovered providers section
|
|
201
|
+
renderDiscoveredProviders();
|
|
202
|
+
|
|
203
|
+
// Update sidebar summary
|
|
204
|
+
const sidebarContainer = document.querySelector('.api-settings-sidebar .embedding-pool-sidebar-summary');
|
|
205
|
+
if (sidebarContainer && sidebarContainer.parentElement) {
|
|
206
|
+
sidebarContainer.parentElement.innerHTML = renderEmbeddingPoolSidebar();
|
|
207
|
+
if (window.lucide) lucide.createIcons();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ========== Provider Management ==========
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Show add provider modal
|
|
215
|
+
*/
|
|
216
|
+
async function showAddProviderModal() {
|
|
217
|
+
const modalHtml = '<div class="generic-modal-overlay active" id="providerModal">' +
|
|
218
|
+
'<div class="generic-modal">' +
|
|
219
|
+
'<div class="generic-modal-header">' +
|
|
220
|
+
'<h3 class="generic-modal-title">' + t('apiSettings.addProvider') + '</h3>' +
|
|
221
|
+
'<button class="generic-modal-close" onclick="closeProviderModal()">×</button>' +
|
|
222
|
+
'</div>' +
|
|
223
|
+
'<div class="generic-modal-body">' +
|
|
224
|
+
'<form id="providerForm" class="api-settings-form">' +
|
|
225
|
+
'<div class="form-group">' +
|
|
226
|
+
'<label for="provider-type">' + t('apiSettings.apiFormat') + '</label>' +
|
|
227
|
+
'<select id="provider-type" class="cli-input" onchange="updateProviderSpecificFields()" required>' +
|
|
228
|
+
'<option value="openai">OpenAI ' + t('apiSettings.compatible') + '</option>' +
|
|
229
|
+
'<option value="anthropic">Anthropic</option>' +
|
|
230
|
+
'<option value="custom">' + t('apiSettings.customFormat') + '</option>' +
|
|
231
|
+
'</select>' +
|
|
232
|
+
'<small class="form-hint">' + t('apiSettings.apiFormatHint') + '</small>' +
|
|
233
|
+
'</div>' +
|
|
234
|
+
'<div class="form-group">' +
|
|
235
|
+
'<label for="provider-name">' + t('apiSettings.displayName') + '</label>' +
|
|
236
|
+
'<input type="text" id="provider-name" class="cli-input" placeholder="My OpenAI" required />' +
|
|
237
|
+
'</div>' +
|
|
238
|
+
'<div class="form-group">' +
|
|
239
|
+
'<label for="provider-apikey">' + t('apiSettings.apiKey') + '</label>' +
|
|
240
|
+
'<div class="api-key-input-group">' +
|
|
241
|
+
'<input type="password" id="provider-apikey" class="cli-input" placeholder="sk-..." required />' +
|
|
242
|
+
'<button type="button" class="btn-icon" onclick="toggleApiKeyVisibility(\'provider-apikey\')" title="' + t('apiSettings.toggleVisibility') + '">' +
|
|
243
|
+
'<i data-lucide="eye"></i>' +
|
|
244
|
+
'</button>' +
|
|
245
|
+
'</div>' +
|
|
246
|
+
'<label class="checkbox-label">' +
|
|
247
|
+
'<input type="checkbox" id="use-env-var" onchange="toggleEnvVarInput()" /> ' +
|
|
248
|
+
t('apiSettings.useEnvVar') +
|
|
249
|
+
'</label>' +
|
|
250
|
+
'<input type="text" id="env-var-name" class="cli-input" placeholder="OPENAI_API_KEY" style="display:none; margin-top: 0.5rem;" />' +
|
|
251
|
+
'</div>' +
|
|
252
|
+
'<div class="form-group">' +
|
|
253
|
+
'<label for="provider-apibase">' + t('apiSettings.apiBaseUrl') + ' <span class="text-muted">(' + t('common.optional') + ')</span></label>' +
|
|
254
|
+
'<input type="text" id="provider-apibase" class="cli-input" placeholder="https://api.openai.com/v1" />' +
|
|
255
|
+
'</div>' +
|
|
256
|
+
'<div class="form-group">' +
|
|
257
|
+
'<label class="checkbox-label">' +
|
|
258
|
+
'<input type="checkbox" id="provider-enabled" checked /> ' +
|
|
259
|
+
t('apiSettings.enableProvider') +
|
|
260
|
+
'</label>' +
|
|
261
|
+
'</div>' +
|
|
262
|
+
// Advanced Settings Collapsible Panel
|
|
263
|
+
'<fieldset class="advanced-settings-fieldset">' +
|
|
264
|
+
'<legend class="advanced-settings-legend" onclick="toggleAdvancedSettings()">' +
|
|
265
|
+
'<i data-lucide="chevron-right" class="advanced-toggle-icon"></i> ' +
|
|
266
|
+
t('apiSettings.advancedSettings') +
|
|
267
|
+
'</legend>' +
|
|
268
|
+
'<div id="advanced-settings-content" class="advanced-settings-content collapsed">' +
|
|
269
|
+
// Timeout
|
|
270
|
+
'<div class="form-group">' +
|
|
271
|
+
'<label for="provider-timeout">' + t('apiSettings.timeout') + ' <span class="text-muted">(' + t('common.optional') + ')</span></label>' +
|
|
272
|
+
'<input type="number" id="provider-timeout" class="cli-input" placeholder="300" min="1" max="3600" />' +
|
|
273
|
+
'<small class="form-hint">' + t('apiSettings.timeoutHint') + '</small>' +
|
|
274
|
+
'</div>' +
|
|
275
|
+
// Max Retries
|
|
276
|
+
'<div class="form-group">' +
|
|
277
|
+
'<label for="provider-max-retries">' + t('apiSettings.maxRetries') + ' <span class="text-muted">(' + t('common.optional') + ')</span></label>' +
|
|
278
|
+
'<input type="number" id="provider-max-retries" class="cli-input" placeholder="3" min="0" max="10" />' +
|
|
279
|
+
'</div>' +
|
|
280
|
+
// Organization (OpenAI only)
|
|
281
|
+
'<div class="form-group provider-specific openai-only" style="display:none;">' +
|
|
282
|
+
'<label for="provider-organization">' + t('apiSettings.organization') + ' <span class="text-muted">(' + t('common.optional') + ')</span></label>' +
|
|
283
|
+
'<input type="text" id="provider-organization" class="cli-input" placeholder="org-..." />' +
|
|
284
|
+
'<small class="form-hint">' + t('apiSettings.organizationHint') + '</small>' +
|
|
285
|
+
'</div>' +
|
|
286
|
+
// API Version (Azure only)
|
|
287
|
+
'<div class="form-group provider-specific azure-only" style="display:none;">' +
|
|
288
|
+
'<label for="provider-api-version">' + t('apiSettings.apiVersion') + ' <span class="text-muted">(' + t('common.optional') + ')</span></label>' +
|
|
289
|
+
'<input type="text" id="provider-api-version" class="cli-input" placeholder="2024-02-01" />' +
|
|
290
|
+
'<small class="form-hint">' + t('apiSettings.apiVersionHint') + '</small>' +
|
|
291
|
+
'</div>' +
|
|
292
|
+
// Rate Limiting (side by side)
|
|
293
|
+
'<div class="form-row">' +
|
|
294
|
+
'<div class="form-group form-group-half">' +
|
|
295
|
+
'<label for="provider-rpm">' + t('apiSettings.rpm') + ' <span class="text-muted">(' + t('common.optional') + ')</span></label>' +
|
|
296
|
+
'<input type="number" id="provider-rpm" class="cli-input" placeholder="' + t('apiSettings.unlimited') + '" min="0" />' +
|
|
297
|
+
'</div>' +
|
|
298
|
+
'<div class="form-group form-group-half">' +
|
|
299
|
+
'<label for="provider-tpm">' + t('apiSettings.tpm') + ' <span class="text-muted">(' + t('common.optional') + ')</span></label>' +
|
|
300
|
+
'<input type="number" id="provider-tpm" class="cli-input" placeholder="' + t('apiSettings.unlimited') + '" min="0" />' +
|
|
301
|
+
'</div>' +
|
|
302
|
+
'</div>' +
|
|
303
|
+
// Proxy
|
|
304
|
+
'<div class="form-group">' +
|
|
305
|
+
'<label for="provider-proxy">' + t('apiSettings.proxy') + ' <span class="text-muted">(' + t('common.optional') + ')</span></label>' +
|
|
306
|
+
'<input type="text" id="provider-proxy" class="cli-input" placeholder="http://proxy.example.com:8080" />' +
|
|
307
|
+
'</div>' +
|
|
308
|
+
// Custom Headers
|
|
309
|
+
'<div class="form-group">' +
|
|
310
|
+
'<label for="provider-custom-headers">' + t('apiSettings.customHeaders') + ' <span class="text-muted">(' + t('common.optional') + ')</span></label>' +
|
|
311
|
+
'<textarea id="provider-custom-headers" class="cli-input cli-textarea" rows="3" placeholder=\'{"X-Custom-Header": "value"}\'></textarea>' +
|
|
312
|
+
'<small class="form-hint">' + t('apiSettings.customHeadersHint') + '</small>' +
|
|
313
|
+
'</div>' +
|
|
314
|
+
'</div>' +
|
|
315
|
+
'</fieldset>' +
|
|
316
|
+
'<div class="modal-actions">' +
|
|
317
|
+
'<button type="button" class="btn btn-secondary" onclick="testProviderConnection()">' +
|
|
318
|
+
'<i data-lucide="wifi"></i> ' + t('apiSettings.testConnection') +
|
|
319
|
+
'</button>' +
|
|
320
|
+
'<button type="button" class="btn btn-secondary" onclick="closeProviderModal()">' + t('common.cancel') + '</button>' +
|
|
321
|
+
'<button type="submit" class="btn btn-primary">' +
|
|
322
|
+
'<i data-lucide="save"></i> ' + t('common.save') +
|
|
323
|
+
'</button>' +
|
|
324
|
+
'</div>' +
|
|
325
|
+
'</form>' +
|
|
326
|
+
'</div>' +
|
|
327
|
+
'</div>' +
|
|
328
|
+
'</div>';
|
|
329
|
+
|
|
330
|
+
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
|
331
|
+
|
|
332
|
+
document.getElementById('providerForm').addEventListener('submit', async function(e) {
|
|
333
|
+
e.preventDefault();
|
|
334
|
+
await saveProvider();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
if (window.lucide) lucide.createIcons();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Show edit provider modal
|
|
342
|
+
*/
|
|
343
|
+
async function showEditProviderModal(providerId) {
|
|
344
|
+
if (!apiSettingsData) return;
|
|
345
|
+
|
|
346
|
+
const provider = apiSettingsData.providers?.find(function(p) { return p.id === providerId; });
|
|
347
|
+
if (!provider) return;
|
|
348
|
+
|
|
349
|
+
await showAddProviderModal();
|
|
350
|
+
|
|
351
|
+
// Update modal title
|
|
352
|
+
document.querySelector('#providerModal .generic-modal-title').textContent = t('apiSettings.editProvider');
|
|
353
|
+
|
|
354
|
+
// Populate form
|
|
355
|
+
document.getElementById('provider-type').value = provider.type;
|
|
356
|
+
document.getElementById('provider-name').value = provider.name;
|
|
357
|
+
document.getElementById('provider-apikey').value = provider.apiKey;
|
|
358
|
+
if (provider.apiBase) {
|
|
359
|
+
document.getElementById('provider-apibase').value = provider.apiBase;
|
|
360
|
+
}
|
|
361
|
+
document.getElementById('provider-enabled').checked = provider.enabled !== false;
|
|
362
|
+
|
|
363
|
+
// Populate advanced settings if they exist
|
|
364
|
+
if (provider.advancedSettings) {
|
|
365
|
+
var settings = provider.advancedSettings;
|
|
366
|
+
|
|
367
|
+
if (settings.timeout) {
|
|
368
|
+
document.getElementById('provider-timeout').value = settings.timeout;
|
|
369
|
+
}
|
|
370
|
+
if (settings.maxRetries !== undefined) {
|
|
371
|
+
document.getElementById('provider-max-retries').value = settings.maxRetries;
|
|
372
|
+
}
|
|
373
|
+
if (settings.organization) {
|
|
374
|
+
document.getElementById('provider-organization').value = settings.organization;
|
|
375
|
+
}
|
|
376
|
+
if (settings.apiVersion) {
|
|
377
|
+
document.getElementById('provider-api-version').value = settings.apiVersion;
|
|
378
|
+
}
|
|
379
|
+
if (settings.rpm) {
|
|
380
|
+
document.getElementById('provider-rpm').value = settings.rpm;
|
|
381
|
+
}
|
|
382
|
+
if (settings.tpm) {
|
|
383
|
+
document.getElementById('provider-tpm').value = settings.tpm;
|
|
384
|
+
}
|
|
385
|
+
if (settings.proxy) {
|
|
386
|
+
document.getElementById('provider-proxy').value = settings.proxy;
|
|
387
|
+
}
|
|
388
|
+
if (settings.customHeaders) {
|
|
389
|
+
document.getElementById('provider-custom-headers').value =
|
|
390
|
+
JSON.stringify(settings.customHeaders, null, 2);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Expand advanced settings if any values exist
|
|
394
|
+
if (Object.keys(settings).length > 0) {
|
|
395
|
+
toggleAdvancedSettings();
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Update provider-specific field visibility
|
|
400
|
+
updateProviderSpecificFields();
|
|
401
|
+
|
|
402
|
+
// Store provider ID for update
|
|
403
|
+
document.getElementById('providerForm').dataset.providerId = providerId;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Save provider (create or update)
|
|
408
|
+
*/
|
|
409
|
+
async function saveProvider() {
|
|
410
|
+
const form = document.getElementById('providerForm');
|
|
411
|
+
const providerId = form.dataset.providerId;
|
|
412
|
+
|
|
413
|
+
const useEnvVar = document.getElementById('use-env-var').checked;
|
|
414
|
+
const apiKey = useEnvVar
|
|
415
|
+
? '${' + document.getElementById('env-var-name').value + '}'
|
|
416
|
+
: document.getElementById('provider-apikey').value;
|
|
417
|
+
|
|
418
|
+
// Collect advanced settings
|
|
419
|
+
var advancedSettings = {};
|
|
420
|
+
|
|
421
|
+
var timeout = document.getElementById('provider-timeout').value;
|
|
422
|
+
if (timeout) advancedSettings.timeout = parseInt(timeout);
|
|
423
|
+
|
|
424
|
+
var maxRetries = document.getElementById('provider-max-retries').value;
|
|
425
|
+
if (maxRetries) advancedSettings.maxRetries = parseInt(maxRetries);
|
|
426
|
+
|
|
427
|
+
var organization = document.getElementById('provider-organization').value;
|
|
428
|
+
if (organization) advancedSettings.organization = organization;
|
|
429
|
+
|
|
430
|
+
var apiVersion = document.getElementById('provider-api-version').value;
|
|
431
|
+
if (apiVersion) advancedSettings.apiVersion = apiVersion;
|
|
432
|
+
|
|
433
|
+
var rpm = document.getElementById('provider-rpm').value;
|
|
434
|
+
if (rpm) advancedSettings.rpm = parseInt(rpm);
|
|
435
|
+
|
|
436
|
+
var tpm = document.getElementById('provider-tpm').value;
|
|
437
|
+
if (tpm) advancedSettings.tpm = parseInt(tpm);
|
|
438
|
+
|
|
439
|
+
var proxy = document.getElementById('provider-proxy').value;
|
|
440
|
+
if (proxy) advancedSettings.proxy = proxy;
|
|
441
|
+
|
|
442
|
+
var customHeadersJson = document.getElementById('provider-custom-headers').value;
|
|
443
|
+
if (customHeadersJson) {
|
|
444
|
+
try {
|
|
445
|
+
advancedSettings.customHeaders = JSON.parse(customHeadersJson);
|
|
446
|
+
} catch (e) {
|
|
447
|
+
showRefreshToast(t('apiSettings.invalidJsonHeaders'), 'error');
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const providerData = {
|
|
453
|
+
type: document.getElementById('provider-type').value,
|
|
454
|
+
name: document.getElementById('provider-name').value,
|
|
455
|
+
apiKey: apiKey,
|
|
456
|
+
apiBase: document.getElementById('provider-apibase').value || undefined,
|
|
457
|
+
enabled: document.getElementById('provider-enabled').checked,
|
|
458
|
+
advancedSettings: Object.keys(advancedSettings).length > 0 ? advancedSettings : undefined
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
try {
|
|
462
|
+
const url = providerId
|
|
463
|
+
? '/api/litellm-api/providers/' + providerId
|
|
464
|
+
: '/api/litellm-api/providers';
|
|
465
|
+
const method = providerId ? 'PUT' : 'POST';
|
|
466
|
+
|
|
467
|
+
const response = await fetch(url, {
|
|
468
|
+
method: method,
|
|
469
|
+
headers: { 'Content-Type': 'application/json' },
|
|
470
|
+
body: JSON.stringify(providerData)
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
if (!response.ok) throw new Error('Failed to save provider');
|
|
474
|
+
|
|
475
|
+
const result = await response.json();
|
|
476
|
+
showRefreshToast(t('apiSettings.providerSaved'), 'success');
|
|
477
|
+
|
|
478
|
+
closeProviderModal();
|
|
479
|
+
// Force refresh data after saving
|
|
480
|
+
apiSettingsData = null;
|
|
481
|
+
await renderApiSettings();
|
|
482
|
+
} catch (err) {
|
|
483
|
+
console.error('Failed to save provider:', err);
|
|
484
|
+
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Delete provider
|
|
490
|
+
*/
|
|
491
|
+
async function deleteProvider(providerId) {
|
|
492
|
+
if (!confirm(t('apiSettings.confirmDeleteProvider'))) return;
|
|
493
|
+
|
|
494
|
+
try {
|
|
495
|
+
const response = await fetch('/api/litellm-api/providers/' + providerId, {
|
|
496
|
+
method: 'DELETE'
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
if (!response.ok) throw new Error('Failed to delete provider');
|
|
500
|
+
|
|
501
|
+
showRefreshToast(t('apiSettings.providerDeleted'), 'success');
|
|
502
|
+
// Force refresh data after deleting
|
|
503
|
+
apiSettingsData = null;
|
|
504
|
+
await renderApiSettings();
|
|
505
|
+
} catch (err) {
|
|
506
|
+
console.error('Failed to delete provider:', err);
|
|
507
|
+
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Test provider connection
|
|
513
|
+
* @param {string} [providerIdParam] - Optional provider ID. If not provided, uses form context or selectedProviderId
|
|
514
|
+
*/
|
|
515
|
+
async function testProviderConnection(providerIdParam) {
|
|
516
|
+
var providerId = providerIdParam;
|
|
517
|
+
|
|
518
|
+
// Try to get providerId from different sources
|
|
519
|
+
if (!providerId) {
|
|
520
|
+
var form = document.getElementById('providerForm');
|
|
521
|
+
if (form && form.dataset.providerId) {
|
|
522
|
+
providerId = form.dataset.providerId;
|
|
523
|
+
} else if (selectedProviderId) {
|
|
524
|
+
providerId = selectedProviderId;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (!providerId) {
|
|
529
|
+
showRefreshToast(t('apiSettings.saveProviderFirst'), 'warning');
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
try {
|
|
534
|
+
const response = await fetch('/api/litellm-api/providers/' + providerId + '/test', {
|
|
535
|
+
method: 'POST'
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
if (!response.ok) throw new Error('Failed to test provider');
|
|
539
|
+
|
|
540
|
+
const result = await response.json();
|
|
541
|
+
|
|
542
|
+
if (result.success) {
|
|
543
|
+
showRefreshToast(t('apiSettings.connectionSuccess'), 'success');
|
|
544
|
+
} else {
|
|
545
|
+
showRefreshToast(t('apiSettings.connectionFailed') + ': ' + (result.error || 'Unknown error'), 'error');
|
|
546
|
+
}
|
|
547
|
+
} catch (err) {
|
|
548
|
+
console.error('Failed to test provider:', err);
|
|
549
|
+
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Close provider modal
|
|
555
|
+
*/
|
|
556
|
+
function closeProviderModal() {
|
|
557
|
+
const modal = document.getElementById('providerModal');
|
|
558
|
+
if (modal) modal.remove();
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Toggle API key visibility
|
|
563
|
+
*/
|
|
564
|
+
function toggleApiKeyVisibility(inputId) {
|
|
565
|
+
const input = document.getElementById(inputId);
|
|
566
|
+
const icon = event.target.closest('button').querySelector('i');
|
|
567
|
+
|
|
568
|
+
if (input.type === 'password') {
|
|
569
|
+
input.type = 'text';
|
|
570
|
+
icon.setAttribute('data-lucide', 'eye-off');
|
|
571
|
+
} else {
|
|
572
|
+
input.type = 'password';
|
|
573
|
+
icon.setAttribute('data-lucide', 'eye');
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (window.lucide) lucide.createIcons();
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Toggle environment variable input
|
|
581
|
+
*/
|
|
582
|
+
function toggleEnvVarInput() {
|
|
583
|
+
const useEnvVar = document.getElementById('use-env-var').checked;
|
|
584
|
+
const apiKeyInput = document.getElementById('provider-apikey');
|
|
585
|
+
const envVarInput = document.getElementById('env-var-name');
|
|
586
|
+
|
|
587
|
+
if (useEnvVar) {
|
|
588
|
+
apiKeyInput.style.display = 'none';
|
|
589
|
+
apiKeyInput.required = false;
|
|
590
|
+
envVarInput.style.display = 'block';
|
|
591
|
+
envVarInput.required = true;
|
|
592
|
+
} else {
|
|
593
|
+
apiKeyInput.style.display = 'block';
|
|
594
|
+
apiKeyInput.required = true;
|
|
595
|
+
envVarInput.style.display = 'none';
|
|
596
|
+
envVarInput.required = false;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Toggle advanced settings visibility
|
|
602
|
+
*/
|
|
603
|
+
function toggleAdvancedSettings() {
|
|
604
|
+
var content = document.getElementById('advanced-settings-content');
|
|
605
|
+
var legend = document.querySelector('.advanced-settings-legend');
|
|
606
|
+
var isCollapsed = content.classList.contains('collapsed');
|
|
607
|
+
|
|
608
|
+
content.classList.toggle('collapsed');
|
|
609
|
+
legend.classList.toggle('expanded');
|
|
610
|
+
|
|
611
|
+
// Update icon
|
|
612
|
+
var icon = legend.querySelector('.advanced-toggle-icon');
|
|
613
|
+
if (icon) {
|
|
614
|
+
icon.setAttribute('data-lucide', isCollapsed ? 'chevron-down' : 'chevron-right');
|
|
615
|
+
if (window.lucide) lucide.createIcons();
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Update provider-specific fields visibility based on provider type
|
|
621
|
+
*/
|
|
622
|
+
function updateProviderSpecificFields() {
|
|
623
|
+
var providerType = document.getElementById('provider-type').value;
|
|
624
|
+
|
|
625
|
+
// Hide all provider-specific fields first
|
|
626
|
+
var specificFields = document.querySelectorAll('.provider-specific');
|
|
627
|
+
specificFields.forEach(function(el) {
|
|
628
|
+
el.style.display = 'none';
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
// Show OpenAI-specific fields
|
|
632
|
+
if (providerType === 'openai') {
|
|
633
|
+
var openaiFields = document.querySelectorAll('.openai-only');
|
|
634
|
+
openaiFields.forEach(function(el) {
|
|
635
|
+
el.style.display = 'block';
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Show Azure-specific fields
|
|
640
|
+
if (providerType === 'azure') {
|
|
641
|
+
var azureFields = document.querySelectorAll('.azure-only');
|
|
642
|
+
azureFields.forEach(function(el) {
|
|
643
|
+
el.style.display = 'block';
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// ========== Endpoint Management ==========
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Show add endpoint modal
|
|
652
|
+
*/
|
|
653
|
+
async function showAddEndpointModal() {
|
|
654
|
+
if (!apiSettingsData || !apiSettingsData.providers || apiSettingsData.providers.length === 0) {
|
|
655
|
+
showRefreshToast(t('apiSettings.addProviderFirst'), 'warning');
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const providerOptions = apiSettingsData.providers
|
|
660
|
+
.filter(function(p) { return p.enabled !== false; })
|
|
661
|
+
.map(function(p) {
|
|
662
|
+
return '<option value="' + p.id + '">' + p.name + ' (' + p.type + ')</option>';
|
|
663
|
+
})
|
|
664
|
+
.join('');
|
|
665
|
+
|
|
666
|
+
const modalHtml = '<div class="generic-modal-overlay active" id="endpointModal">' +
|
|
667
|
+
'<div class="generic-modal">' +
|
|
668
|
+
'<div class="generic-modal-header">' +
|
|
669
|
+
'<h3 class="generic-modal-title">' + t('apiSettings.addEndpoint') + '</h3>' +
|
|
670
|
+
'<button class="generic-modal-close" onclick="closeEndpointModal()">×</button>' +
|
|
671
|
+
'</div>' +
|
|
672
|
+
'<div class="generic-modal-body">' +
|
|
673
|
+
'<form id="endpointForm" class="api-settings-form">' +
|
|
674
|
+
'<div class="form-group">' +
|
|
675
|
+
'<label for="endpoint-id">' + t('apiSettings.endpointId') + '</label>' +
|
|
676
|
+
'<input type="text" id="endpoint-id" class="cli-input" placeholder="my-gpt4o" required />' +
|
|
677
|
+
'<small class="form-hint">' + t('apiSettings.endpointIdHint') + '</small>' +
|
|
678
|
+
'</div>' +
|
|
679
|
+
'<div class="form-group">' +
|
|
680
|
+
'<label for="endpoint-name">' + t('apiSettings.displayName') + '</label>' +
|
|
681
|
+
'<input type="text" id="endpoint-name" class="cli-input" placeholder="GPT-4o for Code Review" required />' +
|
|
682
|
+
'</div>' +
|
|
683
|
+
'<div class="form-group">' +
|
|
684
|
+
'<label for="endpoint-provider">' + t('apiSettings.provider') + '</label>' +
|
|
685
|
+
'<select id="endpoint-provider" class="cli-input" onchange="loadModelsForProvider()" required>' +
|
|
686
|
+
providerOptions +
|
|
687
|
+
'</select>' +
|
|
688
|
+
'</div>' +
|
|
689
|
+
'<div class="form-group">' +
|
|
690
|
+
'<label for="endpoint-model">' + t('apiSettings.model') + '</label>' +
|
|
691
|
+
'<select id="endpoint-model" class="cli-input" required>' +
|
|
692
|
+
'<option value="">' + t('apiSettings.selectModel') + '</option>' +
|
|
693
|
+
'</select>' +
|
|
694
|
+
'</div>' +
|
|
695
|
+
'<fieldset class="form-fieldset">' +
|
|
696
|
+
'<legend>' + t('apiSettings.cacheStrategy') + '</legend>' +
|
|
697
|
+
'<label class="checkbox-label">' +
|
|
698
|
+
'<input type="checkbox" id="cache-enabled" onchange="toggleCacheSettings()" /> ' +
|
|
699
|
+
t('apiSettings.enableContextCaching') +
|
|
700
|
+
'</label>' +
|
|
701
|
+
'<div id="cache-settings" style="display:none;">' +
|
|
702
|
+
'<div class="form-group">' +
|
|
703
|
+
'<label for="cache-ttl">' + t('apiSettings.cacheTTL') + '</label>' +
|
|
704
|
+
'<input type="number" id="cache-ttl" class="cli-input" value="60" min="1" />' +
|
|
705
|
+
'</div>' +
|
|
706
|
+
'<div class="form-group">' +
|
|
707
|
+
'<label for="cache-maxsize">' + t('apiSettings.cacheMaxSize') + '</label>' +
|
|
708
|
+
'<input type="number" id="cache-maxsize" class="cli-input" value="512" min="1" />' +
|
|
709
|
+
'</div>' +
|
|
710
|
+
'<div class="form-group">' +
|
|
711
|
+
'<label for="cache-patterns">' + t('apiSettings.autoCachePatterns') + '</label>' +
|
|
712
|
+
'<input type="text" id="cache-patterns" class="cli-input" placeholder="*.ts, *.md, CLAUDE.md" />' +
|
|
713
|
+
'</div>' +
|
|
714
|
+
'</div>' +
|
|
715
|
+
'</fieldset>' +
|
|
716
|
+
'<div class="modal-actions">' +
|
|
717
|
+
'<button type="button" class="btn btn-secondary" onclick="closeEndpointModal()"><i data-lucide="x"></i> ' + t('common.cancel') + '</button>' +
|
|
718
|
+
'<button type="submit" class="btn btn-primary">' +
|
|
719
|
+
'<i data-lucide="check"></i> ' + t('common.save') +
|
|
720
|
+
'</button>' +
|
|
721
|
+
'</div>' +
|
|
722
|
+
'</form>' +
|
|
723
|
+
'</div>' +
|
|
724
|
+
'</div>' +
|
|
725
|
+
'</div>';
|
|
726
|
+
|
|
727
|
+
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
|
728
|
+
|
|
729
|
+
document.getElementById('endpointForm').addEventListener('submit', async function(e) {
|
|
730
|
+
e.preventDefault();
|
|
731
|
+
await saveEndpoint();
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
// Load models for first provider
|
|
735
|
+
await loadModelsForProvider();
|
|
736
|
+
|
|
737
|
+
if (window.lucide) lucide.createIcons();
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Show edit endpoint modal
|
|
742
|
+
*/
|
|
743
|
+
async function showEditEndpointModal(endpointId) {
|
|
744
|
+
if (!apiSettingsData) return;
|
|
745
|
+
|
|
746
|
+
const endpoint = apiSettingsData.endpoints?.find(function(e) { return e.id === endpointId; });
|
|
747
|
+
if (!endpoint) return;
|
|
748
|
+
|
|
749
|
+
await showAddEndpointModal();
|
|
750
|
+
|
|
751
|
+
// Update modal title
|
|
752
|
+
document.querySelector('#endpointModal .generic-modal-title').textContent = t('apiSettings.editEndpoint');
|
|
753
|
+
|
|
754
|
+
// Populate form
|
|
755
|
+
document.getElementById('endpoint-id').value = endpoint.id;
|
|
756
|
+
document.getElementById('endpoint-id').disabled = true;
|
|
757
|
+
document.getElementById('endpoint-name').value = endpoint.name;
|
|
758
|
+
document.getElementById('endpoint-provider').value = endpoint.providerId;
|
|
759
|
+
|
|
760
|
+
await loadModelsForProvider();
|
|
761
|
+
document.getElementById('endpoint-model').value = endpoint.model;
|
|
762
|
+
|
|
763
|
+
if (endpoint.cacheStrategy) {
|
|
764
|
+
document.getElementById('cache-enabled').checked = endpoint.cacheStrategy.enabled;
|
|
765
|
+
if (endpoint.cacheStrategy.enabled) {
|
|
766
|
+
toggleCacheSettings();
|
|
767
|
+
document.getElementById('cache-ttl').value = endpoint.cacheStrategy.ttlMinutes || 60;
|
|
768
|
+
document.getElementById('cache-maxsize').value = endpoint.cacheStrategy.maxSizeKB || 512;
|
|
769
|
+
document.getElementById('cache-patterns').value = endpoint.cacheStrategy.autoCachePatterns?.join(', ') || '';
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Store endpoint ID for update
|
|
774
|
+
document.getElementById('endpointForm').dataset.endpointId = endpointId;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Save endpoint (create or update)
|
|
779
|
+
*/
|
|
780
|
+
async function saveEndpoint() {
|
|
781
|
+
const form = document.getElementById('endpointForm');
|
|
782
|
+
const endpointId = form.dataset.endpointId || document.getElementById('endpoint-id').value;
|
|
783
|
+
|
|
784
|
+
const cacheEnabled = document.getElementById('cache-enabled').checked;
|
|
785
|
+
const cacheStrategy = cacheEnabled ? {
|
|
786
|
+
enabled: true,
|
|
787
|
+
ttlMinutes: parseInt(document.getElementById('cache-ttl').value) || 60,
|
|
788
|
+
maxSizeKB: parseInt(document.getElementById('cache-maxsize').value) || 512,
|
|
789
|
+
autoCachePatterns: document.getElementById('cache-patterns').value
|
|
790
|
+
.split(',')
|
|
791
|
+
.map(function(p) { return p.trim(); })
|
|
792
|
+
.filter(function(p) { return p; })
|
|
793
|
+
} : { enabled: false };
|
|
794
|
+
|
|
795
|
+
const endpointData = {
|
|
796
|
+
id: endpointId,
|
|
797
|
+
name: document.getElementById('endpoint-name').value,
|
|
798
|
+
providerId: document.getElementById('endpoint-provider').value,
|
|
799
|
+
model: document.getElementById('endpoint-model').value,
|
|
800
|
+
cacheStrategy: cacheStrategy
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
try {
|
|
804
|
+
const url = form.dataset.endpointId
|
|
805
|
+
? '/api/litellm-api/endpoints/' + form.dataset.endpointId
|
|
806
|
+
: '/api/litellm-api/endpoints';
|
|
807
|
+
const method = form.dataset.endpointId ? 'PUT' : 'POST';
|
|
808
|
+
|
|
809
|
+
const response = await fetch(url, {
|
|
810
|
+
method: method,
|
|
811
|
+
headers: { 'Content-Type': 'application/json' },
|
|
812
|
+
body: JSON.stringify(endpointData)
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
if (!response.ok) throw new Error('Failed to save endpoint');
|
|
816
|
+
|
|
817
|
+
const result = await response.json();
|
|
818
|
+
showRefreshToast(t('apiSettings.endpointSaved'), 'success');
|
|
819
|
+
|
|
820
|
+
closeEndpointModal();
|
|
821
|
+
// Force refresh data after saving
|
|
822
|
+
apiSettingsData = null;
|
|
823
|
+
await renderApiSettings();
|
|
824
|
+
} catch (err) {
|
|
825
|
+
console.error('Failed to save endpoint:', err);
|
|
826
|
+
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Delete endpoint
|
|
832
|
+
*/
|
|
833
|
+
async function deleteEndpoint(endpointId) {
|
|
834
|
+
if (!confirm(t('apiSettings.confirmDeleteEndpoint'))) return;
|
|
835
|
+
|
|
836
|
+
try {
|
|
837
|
+
const response = await fetch('/api/litellm-api/endpoints/' + endpointId, {
|
|
838
|
+
method: 'DELETE'
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
if (!response.ok) throw new Error('Failed to delete endpoint');
|
|
842
|
+
|
|
843
|
+
showRefreshToast(t('apiSettings.endpointDeleted'), 'success');
|
|
844
|
+
// Force refresh data after deleting
|
|
845
|
+
apiSettingsData = null;
|
|
846
|
+
await renderApiSettings();
|
|
847
|
+
} catch (err) {
|
|
848
|
+
console.error('Failed to delete endpoint:', err);
|
|
849
|
+
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* Close endpoint modal
|
|
855
|
+
*/
|
|
856
|
+
function closeEndpointModal() {
|
|
857
|
+
const modal = document.getElementById('endpointModal');
|
|
858
|
+
if (modal) modal.remove();
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Load models for selected provider
|
|
863
|
+
*/
|
|
864
|
+
async function loadModelsForProvider() {
|
|
865
|
+
const providerSelect = document.getElementById('endpoint-provider');
|
|
866
|
+
const modelSelect = document.getElementById('endpoint-model');
|
|
867
|
+
|
|
868
|
+
if (!providerSelect || !modelSelect) return;
|
|
869
|
+
|
|
870
|
+
const providerId = providerSelect.value;
|
|
871
|
+
const provider = apiSettingsData.providers.find(function(p) { return p.id === providerId; });
|
|
872
|
+
|
|
873
|
+
if (!provider) return;
|
|
874
|
+
|
|
875
|
+
// Use LLM models configured for this provider (not static presets)
|
|
876
|
+
const models = provider.llmModels || [];
|
|
877
|
+
|
|
878
|
+
if (models.length === 0) {
|
|
879
|
+
modelSelect.innerHTML = '<option value="">' + t('apiSettings.noModelsConfigured') + '</option>';
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
modelSelect.innerHTML = '<option value="">' + t('apiSettings.selectModel') + '</option>' +
|
|
884
|
+
models.filter(function(m) { return m.enabled; }).map(function(m) {
|
|
885
|
+
const contextInfo = m.capabilities && m.capabilities.contextWindow
|
|
886
|
+
? ' (' + Math.round(m.capabilities.contextWindow / 1000) + 'K)'
|
|
887
|
+
: '';
|
|
888
|
+
return '<option value="' + m.id + '">' + m.name + contextInfo + '</option>';
|
|
889
|
+
}).join('');
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* Toggle cache settings visibility
|
|
894
|
+
*/
|
|
895
|
+
function toggleCacheSettings() {
|
|
896
|
+
const enabled = document.getElementById('cache-enabled').checked;
|
|
897
|
+
const settings = document.getElementById('cache-settings');
|
|
898
|
+
settings.style.display = enabled ? 'block' : 'none';
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// ========== Cache Management ==========
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Clear cache
|
|
905
|
+
*/
|
|
906
|
+
async function clearCache() {
|
|
907
|
+
if (!confirm(t('apiSettings.confirmClearCache'))) return;
|
|
908
|
+
|
|
909
|
+
try {
|
|
910
|
+
const response = await fetch('/api/litellm-api/cache/clear', {
|
|
911
|
+
method: 'POST'
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
if (!response.ok) throw new Error('Failed to clear cache');
|
|
915
|
+
|
|
916
|
+
const result = await response.json();
|
|
917
|
+
showRefreshToast(t('apiSettings.cacheCleared') + ' (' + result.removed + ' entries)', 'success');
|
|
918
|
+
|
|
919
|
+
// Cache stats might have changed, but apiSettingsData doesn't need refresh
|
|
920
|
+
await renderApiSettings();
|
|
921
|
+
} catch (err) {
|
|
922
|
+
console.error('Failed to clear cache:', err);
|
|
923
|
+
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Toggle global cache
|
|
929
|
+
*/
|
|
930
|
+
async function toggleGlobalCache() {
|
|
931
|
+
const enabled = document.getElementById('global-cache-enabled').checked;
|
|
932
|
+
|
|
933
|
+
try {
|
|
934
|
+
const response = await fetch('/api/litellm-api/config/cache', {
|
|
935
|
+
method: 'PUT',
|
|
936
|
+
headers: { 'Content-Type': 'application/json' },
|
|
937
|
+
body: JSON.stringify({ enabled: enabled })
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
if (!response.ok) throw new Error('Failed to update cache settings');
|
|
941
|
+
|
|
942
|
+
showRefreshToast(t('apiSettings.cacheSettingsUpdated'), 'success');
|
|
943
|
+
} catch (err) {
|
|
944
|
+
console.error('Failed to update cache settings:', err);
|
|
945
|
+
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
|
|
946
|
+
// Revert checkbox
|
|
947
|
+
document.getElementById('global-cache-enabled').checked = !enabled;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// ========== Rendering ==========
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Render API Settings page - Split Layout
|
|
955
|
+
*/
|
|
956
|
+
async function renderApiSettings() {
|
|
957
|
+
var container = document.getElementById('mainContent');
|
|
958
|
+
if (!container) return;
|
|
959
|
+
|
|
960
|
+
// Hide stats grid and search
|
|
961
|
+
var statsGrid = document.getElementById('statsGrid');
|
|
962
|
+
var searchInput = document.getElementById('searchInput');
|
|
963
|
+
if (statsGrid) statsGrid.style.display = 'none';
|
|
964
|
+
if (searchInput) searchInput.parentElement.style.display = 'none';
|
|
965
|
+
|
|
966
|
+
// Load data (use cache by default, forceRefresh=false)
|
|
967
|
+
await loadApiSettings(false);
|
|
968
|
+
|
|
969
|
+
if (!apiSettingsData) {
|
|
970
|
+
container.innerHTML = '<div class="api-settings-container">' +
|
|
971
|
+
'<div class="error-message">' + t('apiSettings.failedToLoad') + '</div>' +
|
|
972
|
+
'</div>';
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Build sidebar tabs HTML
|
|
977
|
+
var sidebarTabsHtml = '<div class="sidebar-tabs">' +
|
|
978
|
+
'<button class="sidebar-tab' + (activeSidebarTab === 'providers' ? ' active' : '') + '" onclick="switchSidebarTab(\'providers\')">' +
|
|
979
|
+
'<i data-lucide="server"></i> ' + t('apiSettings.providers') +
|
|
980
|
+
'</button>' +
|
|
981
|
+
'<button class="sidebar-tab' + (activeSidebarTab === 'endpoints' ? ' active' : '') + '" onclick="switchSidebarTab(\'endpoints\')">' +
|
|
982
|
+
'<i data-lucide="link"></i> ' + t('apiSettings.endpoints') +
|
|
983
|
+
'</button>' +
|
|
984
|
+
'<button class="sidebar-tab' + (activeSidebarTab === 'embedding-pool' ? ' active' : '') + '" onclick="switchSidebarTab(\'embedding-pool\')">' +
|
|
985
|
+
'<i data-lucide="repeat"></i> ' + t('apiSettings.embeddingPool') +
|
|
986
|
+
'</button>' +
|
|
987
|
+
'<button class="sidebar-tab' + (activeSidebarTab === 'cache' ? ' active' : '') + '" onclick="switchSidebarTab(\'cache\')">' +
|
|
988
|
+
'<i data-lucide="database"></i> ' + t('apiSettings.cache') +
|
|
989
|
+
'</button>' +
|
|
990
|
+
'</div>';
|
|
991
|
+
|
|
992
|
+
// Build sidebar content based on active tab
|
|
993
|
+
var sidebarContentHtml = '';
|
|
994
|
+
var addButtonHtml = '';
|
|
995
|
+
|
|
996
|
+
if (activeSidebarTab === 'providers') {
|
|
997
|
+
sidebarContentHtml = '<div class="provider-search">' +
|
|
998
|
+
'<i data-lucide="search" class="search-icon"></i>' +
|
|
999
|
+
'<input type="text" class="cli-input" id="provider-search-input" placeholder="' + t('apiSettings.searchProviders') + '" oninput="filterProviders(this.value)" />' +
|
|
1000
|
+
'</div>' +
|
|
1001
|
+
'<div class="provider-list" id="provider-list"></div>';
|
|
1002
|
+
addButtonHtml = '<button class="btn btn-primary btn-full" onclick="showAddProviderModal()">' +
|
|
1003
|
+
'<i data-lucide="plus"></i> ' + t('apiSettings.addProvider') +
|
|
1004
|
+
'</button>';
|
|
1005
|
+
} else if (activeSidebarTab === 'endpoints') {
|
|
1006
|
+
sidebarContentHtml = '<div class="endpoints-list" id="endpoints-list"></div>';
|
|
1007
|
+
addButtonHtml = '<button class="btn btn-primary btn-full" onclick="showAddEndpointModal()">' +
|
|
1008
|
+
'<i data-lucide="plus"></i> ' + t('apiSettings.addEndpoint') +
|
|
1009
|
+
'</button>';
|
|
1010
|
+
} else if (activeSidebarTab === 'embedding-pool') {
|
|
1011
|
+
// Load embedding pool config first if not already loaded
|
|
1012
|
+
if (!embeddingPoolConfig) {
|
|
1013
|
+
await loadEmbeddingPoolConfig();
|
|
1014
|
+
}
|
|
1015
|
+
sidebarContentHtml = renderEmbeddingPoolSidebar();
|
|
1016
|
+
} else if (activeSidebarTab === 'cache') {
|
|
1017
|
+
sidebarContentHtml = '<div class="cache-sidebar-info" style="padding: 1rem; color: var(--text-secondary); font-size: 0.875rem;">' +
|
|
1018
|
+
'<p>' + t('apiSettings.cacheTabHint') + '</p>' +
|
|
1019
|
+
'</div>';
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// Build split layout
|
|
1023
|
+
container.innerHTML =
|
|
1024
|
+
// CCW-LiteLLM Status Container
|
|
1025
|
+
'<div id="ccwLitellmStatusContainer" class="mb-4"></div>' +
|
|
1026
|
+
'<div class="api-settings-container api-settings-split">' +
|
|
1027
|
+
// Left Sidebar
|
|
1028
|
+
'<aside class="api-settings-sidebar">' +
|
|
1029
|
+
sidebarTabsHtml +
|
|
1030
|
+
sidebarContentHtml +
|
|
1031
|
+
'<div class="provider-list-footer">' +
|
|
1032
|
+
addButtonHtml +
|
|
1033
|
+
'</div>' +
|
|
1034
|
+
'</aside>' +
|
|
1035
|
+
// Right Main Panel
|
|
1036
|
+
'<main class="api-settings-main" id="provider-detail-panel"></main>' +
|
|
1037
|
+
'</div>' +
|
|
1038
|
+
// Cache Panel Overlay
|
|
1039
|
+
'<div class="cache-panel-overlay" id="cache-panel-overlay" onclick="closeCachePanelOverlay(event)"></div>';
|
|
1040
|
+
|
|
1041
|
+
// Render content based on active tab
|
|
1042
|
+
if (activeSidebarTab === 'providers') {
|
|
1043
|
+
renderProviderList();
|
|
1044
|
+
// Auto-select first provider if exists
|
|
1045
|
+
if (!selectedProviderId && apiSettingsData.providers && apiSettingsData.providers.length > 0) {
|
|
1046
|
+
selectProvider(apiSettingsData.providers[0].id);
|
|
1047
|
+
} else if (selectedProviderId) {
|
|
1048
|
+
renderProviderDetail(selectedProviderId);
|
|
1049
|
+
} else {
|
|
1050
|
+
renderProviderEmptyState();
|
|
1051
|
+
}
|
|
1052
|
+
} else if (activeSidebarTab === 'endpoints') {
|
|
1053
|
+
renderEndpointsList();
|
|
1054
|
+
renderEndpointsMainPanel();
|
|
1055
|
+
} else if (activeSidebarTab === 'embedding-pool') {
|
|
1056
|
+
renderEmbeddingPoolMainPanel();
|
|
1057
|
+
} else if (activeSidebarTab === 'cache') {
|
|
1058
|
+
renderCacheMainPanel();
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// Check and render ccw-litellm status
|
|
1062
|
+
// Force refresh on first load, use cache on subsequent renders
|
|
1063
|
+
const forceStatusRefresh = isFirstApiSettingsRender;
|
|
1064
|
+
if (isFirstApiSettingsRender) {
|
|
1065
|
+
isFirstApiSettingsRender = false;
|
|
1066
|
+
}
|
|
1067
|
+
checkCcwLitellmStatus(forceStatusRefresh).then(renderCcwLitellmStatusCard);
|
|
1068
|
+
|
|
1069
|
+
if (window.lucide) lucide.createIcons();
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* Render provider list in sidebar
|
|
1074
|
+
*/
|
|
1075
|
+
function renderProviderList() {
|
|
1076
|
+
var container = document.getElementById('provider-list');
|
|
1077
|
+
if (!container) return;
|
|
1078
|
+
|
|
1079
|
+
var providers = apiSettingsData.providers || [];
|
|
1080
|
+
var query = providerSearchQuery.toLowerCase();
|
|
1081
|
+
|
|
1082
|
+
// Filter providers
|
|
1083
|
+
if (query) {
|
|
1084
|
+
providers = providers.filter(function(p) {
|
|
1085
|
+
return p.name.toLowerCase().includes(query) || p.type.toLowerCase().includes(query);
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
if (providers.length === 0) {
|
|
1090
|
+
container.innerHTML = '<div class="provider-list-empty">' +
|
|
1091
|
+
'<p>' + (query ? t('apiSettings.noProvidersFound') : t('apiSettings.noProviders')) + '</p>' +
|
|
1092
|
+
'</div>';
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
var html = '';
|
|
1097
|
+
providers.forEach(function(provider) {
|
|
1098
|
+
var isSelected = provider.id === selectedProviderId;
|
|
1099
|
+
var iconClass = getProviderIconClass(provider.type);
|
|
1100
|
+
var iconLetter = provider.type.charAt(0).toUpperCase();
|
|
1101
|
+
|
|
1102
|
+
html += '<div class="provider-list-item' + (isSelected ? ' selected' : '') + '" ' +
|
|
1103
|
+
'data-provider-id="' + provider.id + '" onclick="selectProvider(\'' + provider.id + '\')">' +
|
|
1104
|
+
'<div class="provider-item-icon ' + iconClass + '">' + iconLetter + '</div>' +
|
|
1105
|
+
'<div class="provider-item-info">' +
|
|
1106
|
+
'<span class="provider-item-name">' + escapeHtml(provider.name) + '</span>' +
|
|
1107
|
+
'<span class="provider-item-type">' + provider.type + '</span>' +
|
|
1108
|
+
'</div>' +
|
|
1109
|
+
'<span class="status-badge ' + (provider.enabled ? 'status-enabled' : 'status-disabled') + '">' +
|
|
1110
|
+
(provider.enabled ? 'ON' : 'OFF') +
|
|
1111
|
+
'</span>' +
|
|
1112
|
+
'</div>';
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
container.innerHTML = html;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/**
|
|
1119
|
+
* Filter providers by search query
|
|
1120
|
+
*/
|
|
1121
|
+
function filterProviders(query) {
|
|
1122
|
+
providerSearchQuery = query;
|
|
1123
|
+
renderProviderList();
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
/**
|
|
1127
|
+
* Switch sidebar tab
|
|
1128
|
+
*/
|
|
1129
|
+
function switchSidebarTab(tab) {
|
|
1130
|
+
activeSidebarTab = tab;
|
|
1131
|
+
renderApiSettings();
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
/**
|
|
1135
|
+
* Select a provider
|
|
1136
|
+
*/
|
|
1137
|
+
function selectProvider(providerId) {
|
|
1138
|
+
selectedProviderId = providerId;
|
|
1139
|
+
renderProviderList();
|
|
1140
|
+
renderProviderDetail(providerId);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/**
|
|
1144
|
+
* Render provider detail panel
|
|
1145
|
+
*/
|
|
1146
|
+
function renderProviderDetail(providerId) {
|
|
1147
|
+
var container = document.getElementById('provider-detail-panel');
|
|
1148
|
+
if (!container) return;
|
|
1149
|
+
|
|
1150
|
+
var provider = apiSettingsData.providers.find(function(p) { return p.id === providerId; });
|
|
1151
|
+
if (!provider) {
|
|
1152
|
+
renderProviderEmptyState();
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
var maskedKey = provider.apiKey ? '••••••••••••••••' + provider.apiKey.slice(-4) : '••••••••';
|
|
1157
|
+
var currentApiBase = provider.apiBase || getDefaultApiBase(provider.type);
|
|
1158
|
+
// Show full endpoint URL preview based on active model tab
|
|
1159
|
+
var endpointPath = activeModelTab === 'embedding' ? '/embeddings' : '/chat/completions';
|
|
1160
|
+
var apiBasePreview = currentApiBase + endpointPath;
|
|
1161
|
+
|
|
1162
|
+
var html = '<div class="provider-detail-header">' +
|
|
1163
|
+
'<div class="provider-detail-title">' +
|
|
1164
|
+
'<div class="provider-item-icon ' + getProviderIconClass(provider.type) + '">' +
|
|
1165
|
+
provider.type.charAt(0).toUpperCase() +
|
|
1166
|
+
'</div>' +
|
|
1167
|
+
'<h2>' + escapeHtml(provider.name) + '</h2>' +
|
|
1168
|
+
'<button class="btn-icon-sm" onclick="showEditProviderModal(\'' + providerId + '\')" title="' + t('common.settings') + '">' +
|
|
1169
|
+
'<i data-lucide="settings"></i>' +
|
|
1170
|
+
'</button>' +
|
|
1171
|
+
'<button class="btn-icon-sm text-destructive" onclick="deleteProviderWithConfirm(\'' + providerId + '\')" title="' + t('apiSettings.deleteProvider') + '">' +
|
|
1172
|
+
'<i data-lucide="trash-2"></i>' +
|
|
1173
|
+
'</button>' +
|
|
1174
|
+
'</div>' +
|
|
1175
|
+
'<div class="provider-detail-actions">' +
|
|
1176
|
+
'<label class="toggle-switch">' +
|
|
1177
|
+
'<input type="checkbox" ' + (provider.enabled ? 'checked' : '') + ' onchange="toggleProviderEnabled(\'' + providerId + '\', this.checked)" />' +
|
|
1178
|
+
'<span class="toggle-track"><span class="toggle-thumb"></span></span>' +
|
|
1179
|
+
'</label>' +
|
|
1180
|
+
'</div>' +
|
|
1181
|
+
'</div>' +
|
|
1182
|
+
'<div class="provider-detail-content">' +
|
|
1183
|
+
// API Key field
|
|
1184
|
+
'<div class="field-group">' +
|
|
1185
|
+
'<div class="field-label">' +
|
|
1186
|
+
'<span>' + t('apiSettings.apiKey') + '</span>' +
|
|
1187
|
+
'<div class="field-label-actions">' +
|
|
1188
|
+
'<button class="btn-icon-sm" onclick="copyProviderApiKey(\'' + providerId + '\')" title="' + t('common.copy') + '">' +
|
|
1189
|
+
'<i data-lucide="copy"></i>' +
|
|
1190
|
+
'</button>' +
|
|
1191
|
+
'</div>' +
|
|
1192
|
+
'</div>' +
|
|
1193
|
+
'<div class="field-input-group">' +
|
|
1194
|
+
'<input type="password" class="cli-input" id="provider-detail-apikey" value="' + escapeHtml(provider.apiKey) + '" readonly />' +
|
|
1195
|
+
'<button class="btn-icon" onclick="toggleApiKeyVisibility(\'provider-detail-apikey\')">' +
|
|
1196
|
+
'<i data-lucide="eye"></i>' +
|
|
1197
|
+
'</button>' +
|
|
1198
|
+
'<button class="btn btn-secondary" onclick="testProviderConnection()">' + t('apiSettings.testConnection') + '</button>' +
|
|
1199
|
+
'</div>' +
|
|
1200
|
+
'</div>' +
|
|
1201
|
+
// API Base URL field - editable
|
|
1202
|
+
'<div class="field-group">' +
|
|
1203
|
+
'<div class="field-label">' +
|
|
1204
|
+
'<span>' + t('apiSettings.apiBaseUrl') + '</span>' +
|
|
1205
|
+
'</div>' +
|
|
1206
|
+
'<div class="field-input-group">' +
|
|
1207
|
+
'<input type="text" class="cli-input" id="provider-detail-apibase" value="' + escapeHtml(currentApiBase) + '" placeholder="https://api.openai.com/v1" oninput="updateApiBasePreview(this.value)" />' +
|
|
1208
|
+
'<button class="btn btn-secondary" onclick="saveProviderApiBase(\'' + providerId + '\')">' +
|
|
1209
|
+
'<i data-lucide="save"></i> ' + t('common.save') +
|
|
1210
|
+
'</button>' +
|
|
1211
|
+
'</div>' +
|
|
1212
|
+
'<span class="field-hint" id="api-base-preview">' + t('apiSettings.preview') + ': ' + escapeHtml(apiBasePreview) + '</span>' +
|
|
1213
|
+
'</div>' +
|
|
1214
|
+
// Model Section
|
|
1215
|
+
'<div class="model-section">' +
|
|
1216
|
+
'<div class="model-section-header">' +
|
|
1217
|
+
'<div class="model-tabs">' +
|
|
1218
|
+
'<button class="model-tab' + (activeModelTab === 'llm' ? ' active' : '') + '" onclick="switchModelTab(\'llm\')">' +
|
|
1219
|
+
t('apiSettings.llmModels') +
|
|
1220
|
+
'</button>' +
|
|
1221
|
+
'<button class="model-tab' + (activeModelTab === 'embedding' ? ' active' : '') + '" onclick="switchModelTab(\'embedding\')">' +
|
|
1222
|
+
t('apiSettings.embeddingModels') +
|
|
1223
|
+
'</button>' +
|
|
1224
|
+
'</div>' +
|
|
1225
|
+
'<div class="model-section-actions">' +
|
|
1226
|
+
'<button class="btn btn-secondary" onclick="showManageModelsModal(\'' + providerId + '\')">' +
|
|
1227
|
+
'<i data-lucide="list"></i> ' + t('apiSettings.manageModels') +
|
|
1228
|
+
'</button>' +
|
|
1229
|
+
'<button class="btn btn-primary" onclick="showAddModelModal(\'' + providerId + '\')">' +
|
|
1230
|
+
'<i data-lucide="plus"></i> ' + t('apiSettings.addModel') +
|
|
1231
|
+
'</button>' +
|
|
1232
|
+
'</div>' +
|
|
1233
|
+
'</div>' +
|
|
1234
|
+
'<div class="model-tree" id="model-tree"></div>' +
|
|
1235
|
+
'</div>' +
|
|
1236
|
+
// Multi-key and sync buttons
|
|
1237
|
+
'<div class="multi-key-trigger">' +
|
|
1238
|
+
'<button class="btn btn-secondary multi-key-btn" onclick="showMultiKeyModal(\'' + providerId + '\')">' +
|
|
1239
|
+
'<i data-lucide="key-round"></i> ' + t('apiSettings.multiKeySettings') +
|
|
1240
|
+
'</button>' +
|
|
1241
|
+
'<button class="btn btn-secondary" onclick="syncConfigToCodexLens()">' +
|
|
1242
|
+
'<i data-lucide="refresh-cw"></i> ' + t('apiSettings.syncToCodexLens') +
|
|
1243
|
+
'</button>' +
|
|
1244
|
+
'</div>' +
|
|
1245
|
+
'</div>';
|
|
1246
|
+
|
|
1247
|
+
container.innerHTML = html;
|
|
1248
|
+
renderModelTree(provider);
|
|
1249
|
+
|
|
1250
|
+
if (window.lucide) lucide.createIcons();
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
/**
|
|
1254
|
+
* Render provider empty state
|
|
1255
|
+
*/
|
|
1256
|
+
function renderProviderEmptyState() {
|
|
1257
|
+
var container = document.getElementById('provider-detail-panel');
|
|
1258
|
+
if (!container) return;
|
|
1259
|
+
|
|
1260
|
+
container.innerHTML = '<div class="provider-empty-state">' +
|
|
1261
|
+
'<i data-lucide="database" class="provider-empty-state-icon"></i>' +
|
|
1262
|
+
'<h3>' + t('apiSettings.selectProvider') + '</h3>' +
|
|
1263
|
+
'<p>' + t('apiSettings.selectProviderHint') + '</p>' +
|
|
1264
|
+
'</div>';
|
|
1265
|
+
|
|
1266
|
+
if (window.lucide) lucide.createIcons();
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
/**
|
|
1270
|
+
* Render model tree
|
|
1271
|
+
*/
|
|
1272
|
+
function renderModelTree(provider) {
|
|
1273
|
+
var container = document.getElementById('model-tree');
|
|
1274
|
+
if (!container) return;
|
|
1275
|
+
|
|
1276
|
+
var models = activeModelTab === 'llm'
|
|
1277
|
+
? (provider.llmModels || [])
|
|
1278
|
+
: (provider.embeddingModels || []);
|
|
1279
|
+
|
|
1280
|
+
if (models.length === 0) {
|
|
1281
|
+
container.innerHTML = '<div class="model-tree-empty">' +
|
|
1282
|
+
'<i data-lucide="package" class="model-tree-empty-icon"></i>' +
|
|
1283
|
+
'<p>' + t('apiSettings.noModels') + '</p>' +
|
|
1284
|
+
'</div>';
|
|
1285
|
+
if (window.lucide) lucide.createIcons();
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// Group models by series
|
|
1290
|
+
var groups = groupModelsBySeries(models);
|
|
1291
|
+
|
|
1292
|
+
var html = '';
|
|
1293
|
+
groups.forEach(function(group) {
|
|
1294
|
+
var isExpanded = expandedModelGroups.has(group.series);
|
|
1295
|
+
|
|
1296
|
+
html += '<div class="model-group' + (isExpanded ? ' expanded' : '') + '" data-series="' + escapeHtml(group.series) + '">' +
|
|
1297
|
+
'<div class="model-group-header" onclick="toggleModelGroup(\'' + escapeHtml(group.series) + '\')">' +
|
|
1298
|
+
'<i data-lucide="chevron-right" class="model-group-toggle"></i>' +
|
|
1299
|
+
'<span class="model-group-name">' + escapeHtml(group.series) + '</span>' +
|
|
1300
|
+
'<span class="model-group-count">' + group.models.length + '</span>' +
|
|
1301
|
+
'</div>' +
|
|
1302
|
+
'<div class="model-group-children">';
|
|
1303
|
+
|
|
1304
|
+
group.models.forEach(function(model) {
|
|
1305
|
+
var badge = model.capabilities && model.capabilities.contextWindow
|
|
1306
|
+
? formatContextWindow(model.capabilities.contextWindow)
|
|
1307
|
+
: '';
|
|
1308
|
+
|
|
1309
|
+
// Badge for embedding models shows dimension instead of context window
|
|
1310
|
+
var embeddingBadge = model.capabilities && model.capabilities.embeddingDimension
|
|
1311
|
+
? model.capabilities.embeddingDimension + 'd'
|
|
1312
|
+
: '';
|
|
1313
|
+
var displayBadge = activeModelTab === 'llm' ? badge : embeddingBadge;
|
|
1314
|
+
|
|
1315
|
+
html += '<div class="model-item" data-model-id="' + model.id + '">' +
|
|
1316
|
+
'<i data-lucide="' + (activeModelTab === 'llm' ? 'sparkles' : 'box') + '" class="model-item-icon"></i>' +
|
|
1317
|
+
'<span class="model-item-name">' + escapeHtml(model.name) + '</span>' +
|
|
1318
|
+
(displayBadge ? '<span class="model-item-badge">' + displayBadge + '</span>' : '') +
|
|
1319
|
+
'<div class="model-item-actions">' +
|
|
1320
|
+
'<button class="btn-icon-sm" onclick="showModelSettingsModal(\'' + selectedProviderId + '\', \'' + model.id + '\', \'' + activeModelTab + '\')" title="' + t('apiSettings.modelSettings') + '">' +
|
|
1321
|
+
'<i data-lucide="settings"></i>' +
|
|
1322
|
+
'</button>' +
|
|
1323
|
+
'<button class="btn-icon-sm text-destructive" onclick="deleteModel(\'' + selectedProviderId + '\', \'' + model.id + '\', \'' + activeModelTab + '\')" title="' + t('apiSettings.deleteModel') + '">' +
|
|
1324
|
+
'<i data-lucide="trash-2"></i>' +
|
|
1325
|
+
'</button>' +
|
|
1326
|
+
'</div>' +
|
|
1327
|
+
'</div>';
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
html += '</div></div>';
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
container.innerHTML = html;
|
|
1334
|
+
if (window.lucide) lucide.createIcons();
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/**
|
|
1338
|
+
* Group models by series
|
|
1339
|
+
*/
|
|
1340
|
+
function groupModelsBySeries(models) {
|
|
1341
|
+
var seriesMap = {};
|
|
1342
|
+
|
|
1343
|
+
models.forEach(function(model) {
|
|
1344
|
+
var series = model.series || 'Other';
|
|
1345
|
+
if (!seriesMap[series]) {
|
|
1346
|
+
seriesMap[series] = [];
|
|
1347
|
+
}
|
|
1348
|
+
seriesMap[series].push(model);
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1351
|
+
return Object.keys(seriesMap).map(function(series) {
|
|
1352
|
+
return { series: series, models: seriesMap[series] };
|
|
1353
|
+
}).sort(function(a, b) {
|
|
1354
|
+
return a.series.localeCompare(b.series);
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
/**
|
|
1359
|
+
* Toggle model group expand/collapse
|
|
1360
|
+
*/
|
|
1361
|
+
function toggleModelGroup(series) {
|
|
1362
|
+
if (expandedModelGroups.has(series)) {
|
|
1363
|
+
expandedModelGroups.delete(series);
|
|
1364
|
+
} else {
|
|
1365
|
+
expandedModelGroups.add(series);
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
var provider = apiSettingsData.providers.find(function(p) { return p.id === selectedProviderId; });
|
|
1369
|
+
if (provider) {
|
|
1370
|
+
renderModelTree(provider);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
/**
|
|
1375
|
+
* Switch model tab (LLM / Embedding)
|
|
1376
|
+
*/
|
|
1377
|
+
function switchModelTab(tab) {
|
|
1378
|
+
activeModelTab = tab;
|
|
1379
|
+
expandedModelGroups.clear();
|
|
1380
|
+
|
|
1381
|
+
var provider = apiSettingsData.providers.find(function(p) { return p.id === selectedProviderId; });
|
|
1382
|
+
if (provider) {
|
|
1383
|
+
renderProviderDetail(selectedProviderId);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
/**
|
|
1388
|
+
* Format context window for display
|
|
1389
|
+
*/
|
|
1390
|
+
function formatContextWindow(tokens) {
|
|
1391
|
+
if (tokens >= 1000000) return Math.round(tokens / 1000000) + 'M';
|
|
1392
|
+
if (tokens >= 1000) return Math.round(tokens / 1000) + 'K';
|
|
1393
|
+
return tokens.toString();
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
/**
|
|
1397
|
+
* Get default API base URL for provider type
|
|
1398
|
+
*/
|
|
1399
|
+
function getDefaultApiBase(type) {
|
|
1400
|
+
var defaults = {
|
|
1401
|
+
'openai': 'https://api.openai.com/v1',
|
|
1402
|
+
'anthropic': 'https://api.anthropic.com/v1'
|
|
1403
|
+
};
|
|
1404
|
+
return defaults[type] || 'https://api.example.com/v1';
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
/**
|
|
1408
|
+
* Toggle provider enabled status
|
|
1409
|
+
*/
|
|
1410
|
+
async function toggleProviderEnabled(providerId, enabled) {
|
|
1411
|
+
try {
|
|
1412
|
+
var response = await fetch('/api/litellm-api/providers/' + providerId, {
|
|
1413
|
+
method: 'PUT',
|
|
1414
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1415
|
+
body: JSON.stringify({ enabled: enabled })
|
|
1416
|
+
});
|
|
1417
|
+
if (!response.ok) throw new Error('Failed to update provider');
|
|
1418
|
+
|
|
1419
|
+
// Update local data (for instant UI feedback)
|
|
1420
|
+
var provider = apiSettingsData.providers.find(function(p) { return p.id === providerId; });
|
|
1421
|
+
if (provider) provider.enabled = enabled;
|
|
1422
|
+
|
|
1423
|
+
renderProviderList();
|
|
1424
|
+
showRefreshToast(t('apiSettings.providerUpdated'), 'success');
|
|
1425
|
+
|
|
1426
|
+
// Invalidate cache for next render
|
|
1427
|
+
setTimeout(function() {
|
|
1428
|
+
apiSettingsData = null;
|
|
1429
|
+
}, 100);
|
|
1430
|
+
} catch (err) {
|
|
1431
|
+
console.error('Failed to toggle provider:', err);
|
|
1432
|
+
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
/**
|
|
1437
|
+
* Show cache panel
|
|
1438
|
+
*/
|
|
1439
|
+
async function showCachePanel() {
|
|
1440
|
+
var overlay = document.getElementById('cache-panel-overlay');
|
|
1441
|
+
if (!overlay) return;
|
|
1442
|
+
|
|
1443
|
+
var cacheStats = await loadCacheStats();
|
|
1444
|
+
var usedMB = (cacheStats.totalSize / 1048576).toFixed(1);
|
|
1445
|
+
var maxMB = (cacheStats.maxSize / 1048576).toFixed(0);
|
|
1446
|
+
var usagePercent = cacheStats.maxSize > 0 ? Math.round((cacheStats.totalSize / cacheStats.maxSize) * 100) : 0;
|
|
1447
|
+
|
|
1448
|
+
overlay.innerHTML = '<div class="cache-panel-content" onclick="event.stopPropagation()">' +
|
|
1449
|
+
'<div class="cache-header">' +
|
|
1450
|
+
'<div class="section-title-group">' +
|
|
1451
|
+
'<h3>' + t('apiSettings.cacheSettings') + '</h3>' +
|
|
1452
|
+
'</div>' +
|
|
1453
|
+
'<button class="btn-icon-sm" onclick="closeCachePanel()">' +
|
|
1454
|
+
'<i data-lucide="x"></i>' +
|
|
1455
|
+
'</button>' +
|
|
1456
|
+
'</div>' +
|
|
1457
|
+
'<div class="cache-content">' +
|
|
1458
|
+
'<label class="toggle-switch">' +
|
|
1459
|
+
'<input type="checkbox" id="global-cache-enabled" ' + (cacheStats.enabled ? 'checked' : '') + ' onchange="toggleGlobalCache(this.checked)" />' +
|
|
1460
|
+
'<span class="toggle-track"><span class="toggle-thumb"></span></span>' +
|
|
1461
|
+
'<span class="toggle-label">' + t('apiSettings.enableGlobalCaching') + '</span>' +
|
|
1462
|
+
'</label>' +
|
|
1463
|
+
'<div class="cache-visual">' +
|
|
1464
|
+
'<div class="cache-bars">' +
|
|
1465
|
+
'<div class="cache-bar-fill" style="width: ' + usagePercent + '%"></div>' +
|
|
1466
|
+
'</div>' +
|
|
1467
|
+
'<div class="cache-legend">' +
|
|
1468
|
+
'<span>' + usedMB + ' MB ' + t('apiSettings.used') + '</span>' +
|
|
1469
|
+
'<span>' + maxMB + ' MB ' + t('apiSettings.total') + '</span>' +
|
|
1470
|
+
'</div>' +
|
|
1471
|
+
'</div>' +
|
|
1472
|
+
'<div class="stat-grid">' +
|
|
1473
|
+
'<div class="stat-card">' +
|
|
1474
|
+
'<span class="stat-value">' + usagePercent + '%</span>' +
|
|
1475
|
+
'<span class="stat-desc">' + t('apiSettings.cacheUsage') + '</span>' +
|
|
1476
|
+
'</div>' +
|
|
1477
|
+
'<div class="stat-card">' +
|
|
1478
|
+
'<span class="stat-value">' + cacheStats.entries + '</span>' +
|
|
1479
|
+
'<span class="stat-desc">' + t('apiSettings.cacheEntries') + '</span>' +
|
|
1480
|
+
'</div>' +
|
|
1481
|
+
'</div>' +
|
|
1482
|
+
'<button class="btn btn-secondary btn-full" onclick="clearCache()">' +
|
|
1483
|
+
'<i data-lucide="trash-2"></i> ' + t('apiSettings.clearCache') +
|
|
1484
|
+
'</button>' +
|
|
1485
|
+
'</div>' +
|
|
1486
|
+
'</div>';
|
|
1487
|
+
|
|
1488
|
+
overlay.classList.add('active');
|
|
1489
|
+
if (window.lucide) lucide.createIcons();
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
/**
|
|
1493
|
+
* Close cache panel
|
|
1494
|
+
*/
|
|
1495
|
+
function closeCachePanel() {
|
|
1496
|
+
var overlay = document.getElementById('cache-panel-overlay');
|
|
1497
|
+
if (overlay) {
|
|
1498
|
+
overlay.classList.remove('active');
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
/**
|
|
1503
|
+
* Close cache panel when clicking overlay
|
|
1504
|
+
*/
|
|
1505
|
+
function closeCachePanelOverlay(event) {
|
|
1506
|
+
if (event.target.id === 'cache-panel-overlay') {
|
|
1507
|
+
closeCachePanel();
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
/**
|
|
1512
|
+
* Escape HTML special characters
|
|
1513
|
+
*/
|
|
1514
|
+
function escapeHtml(str) {
|
|
1515
|
+
if (!str) return '';
|
|
1516
|
+
return String(str)
|
|
1517
|
+
.replace(/&/g, '&')
|
|
1518
|
+
.replace(/</g, '<')
|
|
1519
|
+
.replace(/>/g, '>')
|
|
1520
|
+
.replace(/"/g, '"')
|
|
1521
|
+
.replace(/'/g, ''');
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
// ========== Model Management ==========
|
|
1525
|
+
|
|
1526
|
+
/**
|
|
1527
|
+
* Show add model modal
|
|
1528
|
+
*/
|
|
1529
|
+
function showAddModelModal(providerId, modelType) {
|
|
1530
|
+
// Default to active tab if no modelType provided
|
|
1531
|
+
if (!modelType) {
|
|
1532
|
+
modelType = activeModelTab;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// Get provider to know which presets to show
|
|
1536
|
+
const provider = apiSettingsData.providers.find(function(p) { return p.id === providerId; });
|
|
1537
|
+
if (!provider) return;
|
|
1538
|
+
|
|
1539
|
+
const isLlm = modelType === 'llm';
|
|
1540
|
+
const title = isLlm ? t('apiSettings.addLlmModel') : t('apiSettings.addEmbeddingModel');
|
|
1541
|
+
|
|
1542
|
+
// Get model presets based on provider type
|
|
1543
|
+
const presets = isLlm ? getLlmPresetsForType(provider.type) : getEmbeddingPresetsForType(provider.type);
|
|
1544
|
+
|
|
1545
|
+
// Group presets by series
|
|
1546
|
+
const groupedPresets = groupPresetsBySeries(presets);
|
|
1547
|
+
|
|
1548
|
+
const modalHtml = '<div class="generic-modal-overlay active" id="add-model-modal">' +
|
|
1549
|
+
'<div class="generic-modal" style="max-width: 600px;">' +
|
|
1550
|
+
'<div class="generic-modal-header">' +
|
|
1551
|
+
'<h3 class="generic-modal-title">' + title + '</h3>' +
|
|
1552
|
+
'<button class="generic-modal-close" onclick="closeAddModelModal()">×</button>' +
|
|
1553
|
+
'</div>' +
|
|
1554
|
+
'<div class="generic-modal-body">' +
|
|
1555
|
+
'<form id="add-model-form" class="api-settings-form" onsubmit="saveNewModel(event, \'' + providerId + '\', \'' + modelType + '\')">' +
|
|
1556
|
+
|
|
1557
|
+
// Preset Selection
|
|
1558
|
+
'<div class="form-group">' +
|
|
1559
|
+
'<label>' + t('apiSettings.selectFromPresets') + '</label>' +
|
|
1560
|
+
'<select id="model-preset" class="cli-input" onchange="fillModelFromPreset(this.value, \'' + modelType + '\')">' +
|
|
1561
|
+
'<option value="">' + t('apiSettings.customModel') + '</option>' +
|
|
1562
|
+
Object.keys(groupedPresets).map(function(series) {
|
|
1563
|
+
return '<optgroup label="' + series + '">' +
|
|
1564
|
+
groupedPresets[series].map(function(m) {
|
|
1565
|
+
return '<option value="' + m.id + '">' + m.name + ' ' +
|
|
1566
|
+
(isLlm ? '(' + (m.contextWindow/1000) + 'K)' : '(' + m.dimensions + 'D)') +
|
|
1567
|
+
'</option>';
|
|
1568
|
+
}).join('') +
|
|
1569
|
+
'</optgroup>';
|
|
1570
|
+
}).join('') +
|
|
1571
|
+
'</select>' +
|
|
1572
|
+
'</div>' +
|
|
1573
|
+
|
|
1574
|
+
// Model ID
|
|
1575
|
+
'<div class="form-group">' +
|
|
1576
|
+
'<label>' + t('apiSettings.modelId') + ' *</label>' +
|
|
1577
|
+
'<input type="text" id="model-id" class="cli-input" required placeholder="e.g., gpt-4o" />' +
|
|
1578
|
+
'</div>' +
|
|
1579
|
+
|
|
1580
|
+
// Display Name
|
|
1581
|
+
'<div class="form-group">' +
|
|
1582
|
+
'<label>' + t('apiSettings.modelName') + ' *</label>' +
|
|
1583
|
+
'<input type="text" id="model-name" class="cli-input" required placeholder="e.g., GPT-4o" />' +
|
|
1584
|
+
'</div>' +
|
|
1585
|
+
|
|
1586
|
+
// Series
|
|
1587
|
+
'<div class="form-group">' +
|
|
1588
|
+
'<label>' + t('apiSettings.modelSeries') + ' *</label>' +
|
|
1589
|
+
'<input type="text" id="model-series" class="cli-input" required placeholder="e.g., GPT-4" />' +
|
|
1590
|
+
'</div>' +
|
|
1591
|
+
|
|
1592
|
+
// Capabilities based on model type
|
|
1593
|
+
(isLlm ?
|
|
1594
|
+
'<div class="form-group">' +
|
|
1595
|
+
'<label>' + t('apiSettings.contextWindow') + '</label>' +
|
|
1596
|
+
'<input type="number" id="model-context-window" class="cli-input" value="128000" min="1000" />' +
|
|
1597
|
+
'</div>' +
|
|
1598
|
+
'<div class="form-group capabilities-checkboxes">' +
|
|
1599
|
+
'<label style="display: block; margin-bottom: 0.5rem;">' + t('apiSettings.capabilities') + '</label>' +
|
|
1600
|
+
'<label class="checkbox-label">' +
|
|
1601
|
+
'<input type="checkbox" id="cap-streaming" checked /> ' + t('apiSettings.streaming') +
|
|
1602
|
+
'</label>' +
|
|
1603
|
+
'<label class="checkbox-label">' +
|
|
1604
|
+
'<input type="checkbox" id="cap-function-calling" /> ' + t('apiSettings.functionCalling') +
|
|
1605
|
+
'</label>' +
|
|
1606
|
+
'<label class="checkbox-label">' +
|
|
1607
|
+
'<input type="checkbox" id="cap-vision" /> ' + t('apiSettings.vision') +
|
|
1608
|
+
'</label>' +
|
|
1609
|
+
'</div>'
|
|
1610
|
+
:
|
|
1611
|
+
'<div class="form-group">' +
|
|
1612
|
+
'<label>' + t('apiSettings.embeddingDimensions') + ' *</label>' +
|
|
1613
|
+
'<input type="number" id="model-dimensions" class="cli-input" value="1536" min="64" required />' +
|
|
1614
|
+
'</div>' +
|
|
1615
|
+
'<div class="form-group">' +
|
|
1616
|
+
'<label>' + t('apiSettings.embeddingMaxTokens') + '</label>' +
|
|
1617
|
+
'<input type="number" id="model-max-tokens" class="cli-input" value="8192" min="128" />' +
|
|
1618
|
+
'</div>'
|
|
1619
|
+
) +
|
|
1620
|
+
|
|
1621
|
+
// Description
|
|
1622
|
+
'<div class="form-group">' +
|
|
1623
|
+
'<label>' + t('apiSettings.description') + '</label>' +
|
|
1624
|
+
'<textarea id="model-description" class="cli-input" rows="2" placeholder="' + t('apiSettings.optional') + '"></textarea>' +
|
|
1625
|
+
'</div>' +
|
|
1626
|
+
|
|
1627
|
+
'<div class="modal-actions">' +
|
|
1628
|
+
'<button type="button" class="btn btn-secondary" onclick="closeAddModelModal()"><i data-lucide="x"></i> ' + t('common.cancel') + '</button>' +
|
|
1629
|
+
'<button type="submit" class="btn btn-primary"><i data-lucide="check"></i> ' + t('common.save') + '</button>' +
|
|
1630
|
+
'</div>' +
|
|
1631
|
+
'</form>' +
|
|
1632
|
+
'</div>' +
|
|
1633
|
+
'</div>' +
|
|
1634
|
+
'</div>';
|
|
1635
|
+
|
|
1636
|
+
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
|
1637
|
+
if (window.lucide) lucide.createIcons();
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
/**
|
|
1641
|
+
* Close add model modal
|
|
1642
|
+
*/
|
|
1643
|
+
function closeAddModelModal() {
|
|
1644
|
+
const modal = document.getElementById('add-model-modal');
|
|
1645
|
+
if (modal) modal.remove();
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
/**
|
|
1649
|
+
* Get LLM presets for provider type
|
|
1650
|
+
*/
|
|
1651
|
+
function getLlmPresetsForType(providerType) {
|
|
1652
|
+
const presets = {
|
|
1653
|
+
openai: [
|
|
1654
|
+
{ id: 'gpt-4o', name: 'GPT-4o', series: 'GPT-4', contextWindow: 128000 },
|
|
1655
|
+
{ id: 'gpt-4o-mini', name: 'GPT-4o Mini', series: 'GPT-4', contextWindow: 128000 },
|
|
1656
|
+
{ id: 'gpt-4-turbo', name: 'GPT-4 Turbo', series: 'GPT-4', contextWindow: 128000 },
|
|
1657
|
+
{ id: 'gpt-3.5-turbo', name: 'GPT-3.5 Turbo', series: 'GPT-3.5', contextWindow: 16385 },
|
|
1658
|
+
{ id: 'o1', name: 'O1', series: 'O1', contextWindow: 200000 },
|
|
1659
|
+
{ id: 'o1-mini', name: 'O1 Mini', series: 'O1', contextWindow: 128000 },
|
|
1660
|
+
{ id: 'deepseek-chat', name: 'DeepSeek Chat', series: 'DeepSeek', contextWindow: 64000 },
|
|
1661
|
+
{ id: 'deepseek-coder', name: 'DeepSeek Coder', series: 'DeepSeek', contextWindow: 64000 }
|
|
1662
|
+
],
|
|
1663
|
+
anthropic: [
|
|
1664
|
+
{ id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4', series: 'Claude 4', contextWindow: 200000 },
|
|
1665
|
+
{ id: 'claude-3-5-sonnet-20241022', name: 'Claude 3.5 Sonnet', series: 'Claude 3.5', contextWindow: 200000 },
|
|
1666
|
+
{ id: 'claude-3-5-haiku-20241022', name: 'Claude 3.5 Haiku', series: 'Claude 3.5', contextWindow: 200000 },
|
|
1667
|
+
{ id: 'claude-3-opus-20240229', name: 'Claude 3 Opus', series: 'Claude 3', contextWindow: 200000 }
|
|
1668
|
+
],
|
|
1669
|
+
custom: [
|
|
1670
|
+
{ id: 'custom-model', name: 'Custom Model', series: 'Custom', contextWindow: 128000 }
|
|
1671
|
+
]
|
|
1672
|
+
};
|
|
1673
|
+
return presets[providerType] || presets.custom;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
/**
|
|
1677
|
+
* Get Embedding presets for provider type
|
|
1678
|
+
*/
|
|
1679
|
+
function getEmbeddingPresetsForType(providerType) {
|
|
1680
|
+
const presets = {
|
|
1681
|
+
openai: [
|
|
1682
|
+
{ id: 'text-embedding-3-small', name: 'Text Embedding 3 Small', series: 'Embedding V3', dimensions: 1536, maxTokens: 8191 },
|
|
1683
|
+
{ id: 'text-embedding-3-large', name: 'Text Embedding 3 Large', series: 'Embedding V3', dimensions: 3072, maxTokens: 8191 },
|
|
1684
|
+
{ id: 'text-embedding-ada-002', name: 'Ada 002', series: 'Embedding V2', dimensions: 1536, maxTokens: 8191 }
|
|
1685
|
+
],
|
|
1686
|
+
anthropic: [], // Anthropic doesn't have embedding models
|
|
1687
|
+
custom: [
|
|
1688
|
+
{ id: 'custom-embedding', name: 'Custom Embedding', series: 'Custom', dimensions: 1536, maxTokens: 8192 }
|
|
1689
|
+
]
|
|
1690
|
+
};
|
|
1691
|
+
return presets[providerType] || presets.custom;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
/**
|
|
1695
|
+
* Group presets by series
|
|
1696
|
+
*/
|
|
1697
|
+
function groupPresetsBySeries(presets) {
|
|
1698
|
+
const grouped = {};
|
|
1699
|
+
presets.forEach(function(preset) {
|
|
1700
|
+
if (!grouped[preset.series]) {
|
|
1701
|
+
grouped[preset.series] = [];
|
|
1702
|
+
}
|
|
1703
|
+
grouped[preset.series].push(preset);
|
|
1704
|
+
});
|
|
1705
|
+
return grouped;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
/**
|
|
1709
|
+
* Fill model form from preset
|
|
1710
|
+
*/
|
|
1711
|
+
function fillModelFromPreset(presetId, modelType) {
|
|
1712
|
+
if (!presetId) {
|
|
1713
|
+
// Clear fields for custom model
|
|
1714
|
+
document.getElementById('model-id').value = '';
|
|
1715
|
+
document.getElementById('model-name').value = '';
|
|
1716
|
+
document.getElementById('model-series').value = '';
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
const provider = apiSettingsData.providers.find(function(p) { return p.id === selectedProviderId; });
|
|
1721
|
+
if (!provider) return;
|
|
1722
|
+
|
|
1723
|
+
const isLlm = modelType === 'llm';
|
|
1724
|
+
const presets = isLlm ? getLlmPresetsForType(provider.type) : getEmbeddingPresetsForType(provider.type);
|
|
1725
|
+
const preset = presets.find(function(p) { return p.id === presetId; });
|
|
1726
|
+
|
|
1727
|
+
if (preset) {
|
|
1728
|
+
document.getElementById('model-id').value = preset.id;
|
|
1729
|
+
document.getElementById('model-name').value = preset.name;
|
|
1730
|
+
document.getElementById('model-series').value = preset.series;
|
|
1731
|
+
|
|
1732
|
+
if (isLlm && preset.contextWindow) {
|
|
1733
|
+
document.getElementById('model-context-window').value = preset.contextWindow;
|
|
1734
|
+
}
|
|
1735
|
+
if (!isLlm && preset.dimensions) {
|
|
1736
|
+
document.getElementById('model-dimensions').value = preset.dimensions;
|
|
1737
|
+
if (preset.maxTokens) {
|
|
1738
|
+
document.getElementById('model-max-tokens').value = preset.maxTokens;
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
/**
|
|
1745
|
+
* Save new model
|
|
1746
|
+
*/
|
|
1747
|
+
function saveNewModel(event, providerId, modelType) {
|
|
1748
|
+
event.preventDefault();
|
|
1749
|
+
|
|
1750
|
+
const isLlm = modelType === 'llm';
|
|
1751
|
+
const now = new Date().toISOString();
|
|
1752
|
+
|
|
1753
|
+
const newModel = {
|
|
1754
|
+
id: document.getElementById('model-id').value.trim(),
|
|
1755
|
+
name: document.getElementById('model-name').value.trim(),
|
|
1756
|
+
type: modelType,
|
|
1757
|
+
series: document.getElementById('model-series').value.trim(),
|
|
1758
|
+
enabled: true,
|
|
1759
|
+
description: document.getElementById('model-description').value.trim() || undefined,
|
|
1760
|
+
createdAt: now,
|
|
1761
|
+
updatedAt: now
|
|
1762
|
+
};
|
|
1763
|
+
|
|
1764
|
+
// Add capabilities based on model type
|
|
1765
|
+
if (isLlm) {
|
|
1766
|
+
newModel.capabilities = {
|
|
1767
|
+
contextWindow: parseInt(document.getElementById('model-context-window').value) || 128000,
|
|
1768
|
+
streaming: document.getElementById('cap-streaming').checked,
|
|
1769
|
+
functionCalling: document.getElementById('cap-function-calling').checked,
|
|
1770
|
+
vision: document.getElementById('cap-vision').checked
|
|
1771
|
+
};
|
|
1772
|
+
} else {
|
|
1773
|
+
newModel.capabilities = {
|
|
1774
|
+
embeddingDimension: parseInt(document.getElementById('model-dimensions').value) || 1536,
|
|
1775
|
+
contextWindow: parseInt(document.getElementById('model-max-tokens').value) || 8192
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
// Save to provider
|
|
1780
|
+
fetch('/api/litellm-api/providers/' + providerId)
|
|
1781
|
+
.then(function(res) { return res.json(); })
|
|
1782
|
+
.then(function(provider) {
|
|
1783
|
+
const modelsKey = isLlm ? 'llmModels' : 'embeddingModels';
|
|
1784
|
+
const models = provider[modelsKey] || [];
|
|
1785
|
+
|
|
1786
|
+
// Check for duplicate ID
|
|
1787
|
+
if (models.some(function(m) { return m.id === newModel.id; })) {
|
|
1788
|
+
showRefreshToast(t('apiSettings.modelIdExists'), 'error');
|
|
1789
|
+
return Promise.reject('Duplicate ID');
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
models.push(newModel);
|
|
1793
|
+
return fetch('/api/litellm-api/providers/' + providerId, {
|
|
1794
|
+
method: 'PUT',
|
|
1795
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1796
|
+
body: JSON.stringify({ [modelsKey]: models })
|
|
1797
|
+
});
|
|
1798
|
+
})
|
|
1799
|
+
.then(function() {
|
|
1800
|
+
closeAddModelModal();
|
|
1801
|
+
return loadApiSettings();
|
|
1802
|
+
})
|
|
1803
|
+
.then(function() {
|
|
1804
|
+
if (selectedProviderId === providerId) {
|
|
1805
|
+
selectProvider(providerId);
|
|
1806
|
+
}
|
|
1807
|
+
showRefreshToast(t('common.saveSuccess'), 'success');
|
|
1808
|
+
})
|
|
1809
|
+
.catch(function(err) {
|
|
1810
|
+
if (err !== 'Duplicate ID') {
|
|
1811
|
+
console.error('Failed to save model:', err);
|
|
1812
|
+
showRefreshToast(t('common.saveFailed'), 'error');
|
|
1813
|
+
}
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
function showManageModelsModal(providerId) {
|
|
1818
|
+
// For now, show a helpful message
|
|
1819
|
+
showRefreshToast(t('apiSettings.useModelTreeToManage'), 'info');
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
function showModelSettingsModal(providerId, modelId, modelType) {
|
|
1823
|
+
var provider = apiSettingsData.providers.find(function(p) { return p.id === providerId; });
|
|
1824
|
+
if (!provider) return;
|
|
1825
|
+
|
|
1826
|
+
var isLlm = modelType === 'llm';
|
|
1827
|
+
var models = isLlm ? (provider.llmModels || []) : (provider.embeddingModels || []);
|
|
1828
|
+
var model = models.find(function(m) { return m.id === modelId; });
|
|
1829
|
+
if (!model) return;
|
|
1830
|
+
|
|
1831
|
+
var capabilities = model.capabilities || {};
|
|
1832
|
+
var endpointSettings = model.endpointSettings || {};
|
|
1833
|
+
|
|
1834
|
+
// Calculate endpoint preview URL
|
|
1835
|
+
var providerBase = provider.apiBase || getDefaultApiBase(provider.type);
|
|
1836
|
+
var modelBaseUrl = endpointSettings.baseUrl || providerBase;
|
|
1837
|
+
var endpointPath = isLlm ? '/chat/completions' : '/embeddings';
|
|
1838
|
+
var endpointPreview = modelBaseUrl + endpointPath;
|
|
1839
|
+
|
|
1840
|
+
var modalHtml = '<div class="modal-overlay" id="model-settings-modal">' +
|
|
1841
|
+
'<div class="modal-content" style="max-width: 600px;">' +
|
|
1842
|
+
'<div class="modal-header">' +
|
|
1843
|
+
'<h3>' + t('apiSettings.modelSettings') + ': ' + escapeHtml(model.name) + '</h3>' +
|
|
1844
|
+
'<button class="modal-close" onclick="closeModelSettingsModal()">×</button>' +
|
|
1845
|
+
'</div>' +
|
|
1846
|
+
'<div class="modal-body">' +
|
|
1847
|
+
'<form id="model-settings-form" onsubmit="saveModelSettings(event, \'' + providerId + '\', \'' + modelId + '\', \'' + modelType + '\')">' +
|
|
1848
|
+
|
|
1849
|
+
// Endpoint Preview Section (combined view + settings)
|
|
1850
|
+
'<div class="form-section endpoint-preview-section">' +
|
|
1851
|
+
'<h4><i data-lucide="' + (isLlm ? 'message-square' : 'box') + '"></i> ' + t('apiSettings.endpointPreview') + '</h4>' +
|
|
1852
|
+
'<div class="endpoint-preview-box">' +
|
|
1853
|
+
'<code id="model-endpoint-preview">' + escapeHtml(endpointPreview) + '</code>' +
|
|
1854
|
+
'<button type="button" class="btn-icon-sm" onclick="copyModelEndpoint()" title="' + t('common.copy') + '">' +
|
|
1855
|
+
'<i data-lucide="copy"></i>' +
|
|
1856
|
+
'</button>' +
|
|
1857
|
+
'</div>' +
|
|
1858
|
+
'<div class="form-group">' +
|
|
1859
|
+
'<label>' + t('apiSettings.modelBaseUrlOverride') + ' <span class="text-muted">(' + t('common.optional') + ')</span></label>' +
|
|
1860
|
+
'<input type="text" id="model-settings-baseurl" class="cli-input" value="' + escapeHtml(endpointSettings.baseUrl || '') + '" placeholder="' + escapeHtml(providerBase) + '" oninput="updateModelEndpointPreview(\'' + (isLlm ? 'chat/completions' : 'embeddings') + '\', \'' + escapeHtml(providerBase) + '\')">' +
|
|
1861
|
+
'<small class="form-hint">' + t('apiSettings.modelBaseUrlHint') + '</small>' +
|
|
1862
|
+
'</div>' +
|
|
1863
|
+
'</div>' +
|
|
1864
|
+
|
|
1865
|
+
// Basic Info
|
|
1866
|
+
'<div class="form-section">' +
|
|
1867
|
+
'<h4>' + t('apiSettings.basicInfo') + '</h4>' +
|
|
1868
|
+
'<div class="form-group">' +
|
|
1869
|
+
'<label>' + t('apiSettings.modelName') + '</label>' +
|
|
1870
|
+
'<input type="text" id="model-settings-name" class="cli-input" value="' + escapeHtml(model.name || '') + '" required>' +
|
|
1871
|
+
'</div>' +
|
|
1872
|
+
'<div class="form-group">' +
|
|
1873
|
+
'<label>' + t('apiSettings.modelSeries') + '</label>' +
|
|
1874
|
+
'<input type="text" id="model-settings-series" class="cli-input" value="' + escapeHtml(model.series || '') + '" required>' +
|
|
1875
|
+
'</div>' +
|
|
1876
|
+
'<div class="form-group">' +
|
|
1877
|
+
'<label>' + t('apiSettings.description') + '</label>' +
|
|
1878
|
+
'<textarea id="model-settings-description" class="cli-input" rows="2">' + escapeHtml(model.description || '') + '</textarea>' +
|
|
1879
|
+
'</div>' +
|
|
1880
|
+
'</div>' +
|
|
1881
|
+
|
|
1882
|
+
// Capabilities
|
|
1883
|
+
'<div class="form-section">' +
|
|
1884
|
+
'<h4>' + t('apiSettings.capabilities') + '</h4>' +
|
|
1885
|
+
(isLlm ? (
|
|
1886
|
+
'<div class="form-group">' +
|
|
1887
|
+
'<label>' + t('apiSettings.contextWindow') + '</label>' +
|
|
1888
|
+
'<input type="number" id="model-settings-context" class="cli-input" value="' + (capabilities.contextWindow || 128000) + '" min="1000">' +
|
|
1889
|
+
'</div>' +
|
|
1890
|
+
'<div class="form-group capabilities-checkboxes">' +
|
|
1891
|
+
'<label class="checkbox-label"><input type="checkbox" id="model-settings-streaming"' + (capabilities.streaming ? ' checked' : '') + '> ' + t('apiSettings.streaming') + '</label>' +
|
|
1892
|
+
'<label class="checkbox-label"><input type="checkbox" id="model-settings-function-calling"' + (capabilities.functionCalling ? ' checked' : '') + '> ' + t('apiSettings.functionCalling') + '</label>' +
|
|
1893
|
+
'<label class="checkbox-label"><input type="checkbox" id="model-settings-vision"' + (capabilities.vision ? ' checked' : '') + '> ' + t('apiSettings.vision') + '</label>' +
|
|
1894
|
+
'</div>'
|
|
1895
|
+
) : (
|
|
1896
|
+
'<div class="form-group">' +
|
|
1897
|
+
'<label>' + t('apiSettings.embeddingDimensions') + '</label>' +
|
|
1898
|
+
'<input type="number" id="model-settings-dimensions" class="cli-input" value="' + (capabilities.embeddingDimension || 1536) + '" min="64">' +
|
|
1899
|
+
'</div>' +
|
|
1900
|
+
'<div class="form-group">' +
|
|
1901
|
+
'<label>' + t('apiSettings.embeddingMaxTokens') + '</label>' +
|
|
1902
|
+
'<input type="number" id="model-settings-max-tokens" class="cli-input" value="' + (capabilities.contextWindow || 8192) + '" min="128">' +
|
|
1903
|
+
'</div>'
|
|
1904
|
+
)) +
|
|
1905
|
+
'</div>' +
|
|
1906
|
+
|
|
1907
|
+
// Endpoint Settings
|
|
1908
|
+
'<div class="form-section">' +
|
|
1909
|
+
'<h4>' + t('apiSettings.endpointSettings') + '</h4>' +
|
|
1910
|
+
'<div class="form-row">' +
|
|
1911
|
+
'<div class="form-group form-group-half">' +
|
|
1912
|
+
'<label>' + t('apiSettings.timeout') + ' (' + t('apiSettings.seconds') + ')</label>' +
|
|
1913
|
+
'<input type="number" id="model-settings-timeout" class="cli-input" value="' + (endpointSettings.timeout || 300) + '" min="10" max="3600">' +
|
|
1914
|
+
'</div>' +
|
|
1915
|
+
'<div class="form-group form-group-half">' +
|
|
1916
|
+
'<label>' + t('apiSettings.maxRetries') + '</label>' +
|
|
1917
|
+
'<input type="number" id="model-settings-retries" class="cli-input" value="' + (endpointSettings.maxRetries || 3) + '" min="0" max="10">' +
|
|
1918
|
+
'</div>' +
|
|
1919
|
+
'</div>' +
|
|
1920
|
+
'</div>' +
|
|
1921
|
+
|
|
1922
|
+
'<div class="modal-actions">' +
|
|
1923
|
+
'<button type="button" class="btn-secondary" onclick="closeModelSettingsModal()"><i data-lucide="x"></i> ' + t('common.cancel') + '</button>' +
|
|
1924
|
+
'<button type="submit" class="btn-primary"><i data-lucide="check"></i> ' + t('common.save') + '</button>' +
|
|
1925
|
+
'</div>' +
|
|
1926
|
+
'</form>' +
|
|
1927
|
+
'</div>' +
|
|
1928
|
+
'</div>' +
|
|
1929
|
+
'</div>';
|
|
1930
|
+
|
|
1931
|
+
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
|
1932
|
+
if (window.lucide) lucide.createIcons();
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
/**
|
|
1936
|
+
* Update model endpoint preview when base URL changes
|
|
1937
|
+
*/
|
|
1938
|
+
function updateModelEndpointPreview(endpointPath, defaultBase) {
|
|
1939
|
+
var baseUrlInput = document.getElementById('model-settings-baseurl');
|
|
1940
|
+
var previewElement = document.getElementById('model-endpoint-preview');
|
|
1941
|
+
if (!baseUrlInput || !previewElement) return;
|
|
1942
|
+
|
|
1943
|
+
var baseUrl = baseUrlInput.value.trim() || defaultBase;
|
|
1944
|
+
// Remove trailing slash if present
|
|
1945
|
+
if (baseUrl.endsWith('/')) {
|
|
1946
|
+
baseUrl = baseUrl.slice(0, -1);
|
|
1947
|
+
}
|
|
1948
|
+
previewElement.textContent = baseUrl + '/' + endpointPath;
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
/**
|
|
1952
|
+
* Copy model endpoint URL to clipboard
|
|
1953
|
+
*/
|
|
1954
|
+
function copyModelEndpoint() {
|
|
1955
|
+
var previewElement = document.getElementById('model-endpoint-preview');
|
|
1956
|
+
if (previewElement) {
|
|
1957
|
+
navigator.clipboard.writeText(previewElement.textContent);
|
|
1958
|
+
showRefreshToast(t('common.copied'), 'success');
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
function closeModelSettingsModal() {
|
|
1963
|
+
var modal = document.getElementById('model-settings-modal');
|
|
1964
|
+
if (modal) modal.remove();
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
function saveModelSettings(event, providerId, modelId, modelType) {
|
|
1968
|
+
event.preventDefault();
|
|
1969
|
+
|
|
1970
|
+
var isLlm = modelType === 'llm';
|
|
1971
|
+
var modelsKey = isLlm ? 'llmModels' : 'embeddingModels';
|
|
1972
|
+
|
|
1973
|
+
fetch('/api/litellm-api/providers/' + providerId)
|
|
1974
|
+
.then(function(res) { return res.json(); })
|
|
1975
|
+
.then(function(provider) {
|
|
1976
|
+
var models = provider[modelsKey] || [];
|
|
1977
|
+
var modelIndex = models.findIndex(function(m) { return m.id === modelId; });
|
|
1978
|
+
|
|
1979
|
+
if (modelIndex === -1) {
|
|
1980
|
+
throw new Error('Model not found');
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// Update model fields
|
|
1984
|
+
models[modelIndex].name = document.getElementById('model-settings-name').value.trim();
|
|
1985
|
+
models[modelIndex].series = document.getElementById('model-settings-series').value.trim();
|
|
1986
|
+
models[modelIndex].description = document.getElementById('model-settings-description').value.trim() || undefined;
|
|
1987
|
+
models[modelIndex].updatedAt = new Date().toISOString();
|
|
1988
|
+
|
|
1989
|
+
// Update capabilities
|
|
1990
|
+
if (isLlm) {
|
|
1991
|
+
models[modelIndex].capabilities = {
|
|
1992
|
+
contextWindow: parseInt(document.getElementById('model-settings-context').value) || 128000,
|
|
1993
|
+
streaming: document.getElementById('model-settings-streaming').checked,
|
|
1994
|
+
functionCalling: document.getElementById('model-settings-function-calling').checked,
|
|
1995
|
+
vision: document.getElementById('model-settings-vision').checked
|
|
1996
|
+
};
|
|
1997
|
+
} else {
|
|
1998
|
+
models[modelIndex].capabilities = {
|
|
1999
|
+
embeddingDimension: parseInt(document.getElementById('model-settings-dimensions').value) || 1536,
|
|
2000
|
+
contextWindow: parseInt(document.getElementById('model-settings-max-tokens').value) || 8192
|
|
2001
|
+
};
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
// Update endpoint settings
|
|
2005
|
+
var baseUrlOverride = document.getElementById('model-settings-baseurl').value.trim();
|
|
2006
|
+
// Remove trailing slash if present
|
|
2007
|
+
if (baseUrlOverride && baseUrlOverride.endsWith('/')) {
|
|
2008
|
+
baseUrlOverride = baseUrlOverride.slice(0, -1);
|
|
2009
|
+
}
|
|
2010
|
+
models[modelIndex].endpointSettings = {
|
|
2011
|
+
baseUrl: baseUrlOverride || undefined,
|
|
2012
|
+
timeout: parseInt(document.getElementById('model-settings-timeout').value) || 300,
|
|
2013
|
+
maxRetries: parseInt(document.getElementById('model-settings-retries').value) || 3
|
|
2014
|
+
};
|
|
2015
|
+
|
|
2016
|
+
var updateData = {};
|
|
2017
|
+
updateData[modelsKey] = models;
|
|
2018
|
+
|
|
2019
|
+
return fetch('/api/litellm-api/providers/' + providerId, {
|
|
2020
|
+
method: 'PUT',
|
|
2021
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2022
|
+
body: JSON.stringify(updateData)
|
|
2023
|
+
});
|
|
2024
|
+
})
|
|
2025
|
+
.then(function() {
|
|
2026
|
+
closeModelSettingsModal();
|
|
2027
|
+
return loadApiSettings();
|
|
2028
|
+
})
|
|
2029
|
+
.then(function() {
|
|
2030
|
+
if (selectedProviderId === providerId) {
|
|
2031
|
+
selectProvider(providerId);
|
|
2032
|
+
}
|
|
2033
|
+
showRefreshToast(t('common.saveSuccess'), 'success');
|
|
2034
|
+
})
|
|
2035
|
+
.catch(function(err) {
|
|
2036
|
+
console.error('Failed to save model settings:', err);
|
|
2037
|
+
showRefreshToast(t('common.saveFailed'), 'error');
|
|
2038
|
+
});
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
function deleteModel(providerId, modelId, modelType) {
|
|
2042
|
+
if (!confirm(t('common.confirmDelete'))) return;
|
|
2043
|
+
|
|
2044
|
+
var isLlm = modelType === 'llm';
|
|
2045
|
+
var modelsKey = isLlm ? 'llmModels' : 'embeddingModels';
|
|
2046
|
+
|
|
2047
|
+
fetch('/api/litellm-api/providers/' + providerId)
|
|
2048
|
+
.then(function(res) { return res.json(); })
|
|
2049
|
+
.then(function(provider) {
|
|
2050
|
+
var models = provider[modelsKey] || [];
|
|
2051
|
+
var updatedModels = models.filter(function(m) { return m.id !== modelId; });
|
|
2052
|
+
|
|
2053
|
+
var updateData = {};
|
|
2054
|
+
updateData[modelsKey] = updatedModels;
|
|
2055
|
+
|
|
2056
|
+
return fetch('/api/litellm-api/providers/' + providerId, {
|
|
2057
|
+
method: 'PUT',
|
|
2058
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2059
|
+
body: JSON.stringify(updateData)
|
|
2060
|
+
});
|
|
2061
|
+
})
|
|
2062
|
+
.then(function() {
|
|
2063
|
+
return loadApiSettings();
|
|
2064
|
+
})
|
|
2065
|
+
.then(function() {
|
|
2066
|
+
if (selectedProviderId === providerId) {
|
|
2067
|
+
selectProvider(providerId);
|
|
2068
|
+
}
|
|
2069
|
+
showRefreshToast(t('common.deleteSuccess'), 'success');
|
|
2070
|
+
})
|
|
2071
|
+
.catch(function(err) {
|
|
2072
|
+
console.error('Failed to delete model:', err);
|
|
2073
|
+
showRefreshToast(t('common.deleteFailed'), 'error');
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
function copyProviderApiKey(providerId) {
|
|
2078
|
+
var provider = apiSettingsData.providers.find(function(p) { return p.id === providerId; });
|
|
2079
|
+
if (provider && provider.apiKey) {
|
|
2080
|
+
navigator.clipboard.writeText(provider.apiKey);
|
|
2081
|
+
showRefreshToast(t('common.copied'), 'success');
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
/**
|
|
2086
|
+
* Save provider API base URL
|
|
2087
|
+
*/
|
|
2088
|
+
async function saveProviderApiBase(providerId) {
|
|
2089
|
+
var input = document.getElementById('provider-detail-apibase');
|
|
2090
|
+
if (!input) return;
|
|
2091
|
+
|
|
2092
|
+
var newApiBase = input.value.trim();
|
|
2093
|
+
// Remove trailing slash if present
|
|
2094
|
+
if (newApiBase.endsWith('/')) {
|
|
2095
|
+
newApiBase = newApiBase.slice(0, -1);
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
try {
|
|
2099
|
+
var response = await fetch('/api/litellm-api/providers/' + providerId, {
|
|
2100
|
+
method: 'PUT',
|
|
2101
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2102
|
+
body: JSON.stringify({ apiBase: newApiBase || undefined })
|
|
2103
|
+
});
|
|
2104
|
+
|
|
2105
|
+
if (!response.ok) throw new Error('Failed to update API base');
|
|
2106
|
+
|
|
2107
|
+
// Update local data (for instant UI feedback)
|
|
2108
|
+
var provider = apiSettingsData.providers.find(function(p) { return p.id === providerId; });
|
|
2109
|
+
if (provider) {
|
|
2110
|
+
provider.apiBase = newApiBase || undefined;
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
// Update preview
|
|
2114
|
+
updateApiBasePreview(newApiBase);
|
|
2115
|
+
showRefreshToast(t('apiSettings.apiBaseUpdated'), 'success');
|
|
2116
|
+
|
|
2117
|
+
// Invalidate cache for next render (but keep current data for immediate UI)
|
|
2118
|
+
// This ensures next tab switch or page refresh gets fresh data
|
|
2119
|
+
setTimeout(function() {
|
|
2120
|
+
apiSettingsData = null;
|
|
2121
|
+
}, 100);
|
|
2122
|
+
} catch (err) {
|
|
2123
|
+
console.error('Failed to save API base:', err);
|
|
2124
|
+
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
/**
|
|
2129
|
+
* Update API base preview text showing full endpoint URL
|
|
2130
|
+
*/
|
|
2131
|
+
function updateApiBasePreview(apiBase) {
|
|
2132
|
+
var preview = document.getElementById('api-base-preview');
|
|
2133
|
+
if (!preview) return;
|
|
2134
|
+
|
|
2135
|
+
var base = apiBase || getDefaultApiBase('openai');
|
|
2136
|
+
// Remove trailing slash if present
|
|
2137
|
+
if (base.endsWith('/')) {
|
|
2138
|
+
base = base.slice(0, -1);
|
|
2139
|
+
}
|
|
2140
|
+
var endpointPath = activeModelTab === 'embedding' ? '/embeddings' : '/chat/completions';
|
|
2141
|
+
preview.textContent = t('apiSettings.preview') + ': ' + base + endpointPath;
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
/**
|
|
2145
|
+
* Delete provider with confirmation
|
|
2146
|
+
*/
|
|
2147
|
+
async function deleteProviderWithConfirm(providerId) {
|
|
2148
|
+
if (!confirm(t('apiSettings.confirmDeleteProvider'))) return;
|
|
2149
|
+
|
|
2150
|
+
try {
|
|
2151
|
+
var response = await fetch('/api/litellm-api/providers/' + providerId, {
|
|
2152
|
+
method: 'DELETE'
|
|
2153
|
+
});
|
|
2154
|
+
|
|
2155
|
+
if (!response.ok) throw new Error('Failed to delete provider');
|
|
2156
|
+
|
|
2157
|
+
// Remove from local data
|
|
2158
|
+
apiSettingsData.providers = apiSettingsData.providers.filter(function(p) {
|
|
2159
|
+
return p.id !== providerId;
|
|
2160
|
+
});
|
|
2161
|
+
|
|
2162
|
+
// Clear selection if deleted provider was selected
|
|
2163
|
+
if (selectedProviderId === providerId) {
|
|
2164
|
+
selectedProviderId = null;
|
|
2165
|
+
if (apiSettingsData.providers.length > 0) {
|
|
2166
|
+
selectProvider(apiSettingsData.providers[0].id);
|
|
2167
|
+
} else {
|
|
2168
|
+
renderProviderEmptyState();
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
renderProviderList();
|
|
2173
|
+
showRefreshToast(t('apiSettings.providerDeleted'), 'success');
|
|
2174
|
+
} catch (err) {
|
|
2175
|
+
console.error('Failed to delete provider:', err);
|
|
2176
|
+
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
/**
|
|
2181
|
+
* Sync config to CodexLens (generate YAML config for ccw_litellm)
|
|
2182
|
+
*/
|
|
2183
|
+
async function syncConfigToCodexLens() {
|
|
2184
|
+
try {
|
|
2185
|
+
var response = await fetch('/api/litellm-api/config/sync', {
|
|
2186
|
+
method: 'POST'
|
|
2187
|
+
});
|
|
2188
|
+
|
|
2189
|
+
if (!response.ok) throw new Error('Failed to sync config');
|
|
2190
|
+
|
|
2191
|
+
var result = await response.json();
|
|
2192
|
+
showRefreshToast(t('apiSettings.configSynced') + ' (' + result.yamlPath + ')', 'success');
|
|
2193
|
+
} catch (err) {
|
|
2194
|
+
console.error('Failed to sync config:', err);
|
|
2195
|
+
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
/**
|
|
2200
|
+
* Get provider icon class based on type
|
|
2201
|
+
*/
|
|
2202
|
+
function getProviderIconClass(type) {
|
|
2203
|
+
var iconMap = {
|
|
2204
|
+
'openai': 'provider-icon-openai',
|
|
2205
|
+
'anthropic': 'provider-icon-anthropic'
|
|
2206
|
+
};
|
|
2207
|
+
return iconMap[type] || 'provider-icon-custom';
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
/**
|
|
2211
|
+
* Get provider icon name based on type
|
|
2212
|
+
*/
|
|
2213
|
+
function getProviderIcon(type) {
|
|
2214
|
+
const iconMap = {
|
|
2215
|
+
'openai': 'sparkles',
|
|
2216
|
+
'anthropic': 'brain',
|
|
2217
|
+
'google': 'cloud',
|
|
2218
|
+
'azure': 'cloud-cog',
|
|
2219
|
+
'ollama': 'server',
|
|
2220
|
+
'mistral': 'wind',
|
|
2221
|
+
'deepseek': 'search'
|
|
2222
|
+
};
|
|
2223
|
+
return iconMap[type] || 'settings';
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
/**
|
|
2227
|
+
* Render providers list
|
|
2228
|
+
*/
|
|
2229
|
+
function renderProvidersList() {
|
|
2230
|
+
const container = document.getElementById('providers-list');
|
|
2231
|
+
if (!container) return;
|
|
2232
|
+
|
|
2233
|
+
const providers = apiSettingsData.providers || [];
|
|
2234
|
+
|
|
2235
|
+
if (providers.length === 0) {
|
|
2236
|
+
container.innerHTML = '<div class="empty-state">' +
|
|
2237
|
+
'<div class="empty-icon-wrapper">' +
|
|
2238
|
+
'<i data-lucide="cloud-off"></i>' +
|
|
2239
|
+
'</div>' +
|
|
2240
|
+
'<h4>' + t('apiSettings.noProviders') + '</h4>' +
|
|
2241
|
+
'<p>' + t('apiSettings.noProvidersHint') + '</p>' +
|
|
2242
|
+
'</div>';
|
|
2243
|
+
if (window.lucide) lucide.createIcons();
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
container.innerHTML = providers.map(function(provider) {
|
|
2248
|
+
const statusClass = provider.enabled === false ? 'disabled' : 'enabled';
|
|
2249
|
+
const statusText = provider.enabled === false ? t('apiSettings.disabled') : t('apiSettings.enabled');
|
|
2250
|
+
const iconClass = getProviderIconClass(provider.type);
|
|
2251
|
+
const iconName = getProviderIcon(provider.type);
|
|
2252
|
+
|
|
2253
|
+
return '<div class="api-card' + (provider.enabled === false ? ' disabled' : '') + '">' +
|
|
2254
|
+
'<div class="card-header">' +
|
|
2255
|
+
'<div class="card-title-group">' +
|
|
2256
|
+
'<div class="card-icon ' + iconClass + '">' +
|
|
2257
|
+
'<i data-lucide="' + iconName + '"></i>' +
|
|
2258
|
+
'</div>' +
|
|
2259
|
+
'<div class="card-info">' +
|
|
2260
|
+
'<h4 class="card-title">' + provider.name + '</h4>' +
|
|
2261
|
+
'<span class="card-subtitle"><span class="provider-type-badge">' + provider.type + '</span></span>' +
|
|
2262
|
+
'</div>' +
|
|
2263
|
+
'</div>' +
|
|
2264
|
+
'<div class="card-actions">' +
|
|
2265
|
+
'<button class="btn-icon-sm" onclick="showEditProviderModal(\'' + provider.id + '\')" title="' + t('common.edit') + '">' +
|
|
2266
|
+
'<i data-lucide="pencil"></i>' +
|
|
2267
|
+
'</button>' +
|
|
2268
|
+
'<button class="btn-icon-sm text-destructive" onclick="deleteProvider(\'' + provider.id + '\')" title="' + t('common.delete') + '">' +
|
|
2269
|
+
'<i data-lucide="trash-2"></i>' +
|
|
2270
|
+
'</button>' +
|
|
2271
|
+
'</div>' +
|
|
2272
|
+
'</div>' +
|
|
2273
|
+
'<div class="card-body">' +
|
|
2274
|
+
'<div class="card-meta-grid">' +
|
|
2275
|
+
'<div class="meta-item">' +
|
|
2276
|
+
'<span class="meta-label">' + t('apiSettings.apiKey') + '</span>' +
|
|
2277
|
+
'<span class="meta-value">' + maskApiKey(provider.apiKey) + '</span>' +
|
|
2278
|
+
'</div>' +
|
|
2279
|
+
'<div class="meta-item">' +
|
|
2280
|
+
'<span class="meta-label">' + t('common.status') + '</span>' +
|
|
2281
|
+
'<span class="status-badge status-' + statusClass + '">' + statusText + '</span>' +
|
|
2282
|
+
'</div>' +
|
|
2283
|
+
(provider.apiBase ?
|
|
2284
|
+
'<div class="meta-item" style="grid-column: span 2;">' +
|
|
2285
|
+
'<span class="meta-label">' + t('apiSettings.apiBaseUrl') + '</span>' +
|
|
2286
|
+
'<span class="meta-value">' + provider.apiBase + '</span>' +
|
|
2287
|
+
'</div>' : '') +
|
|
2288
|
+
'</div>' +
|
|
2289
|
+
'</div>' +
|
|
2290
|
+
'</div>';
|
|
2291
|
+
}).join('');
|
|
2292
|
+
|
|
2293
|
+
if (window.lucide) lucide.createIcons();
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
/**
|
|
2297
|
+
* Render endpoints list
|
|
2298
|
+
*/
|
|
2299
|
+
function renderEndpointsList() {
|
|
2300
|
+
const container = document.getElementById('endpoints-list');
|
|
2301
|
+
if (!container) return;
|
|
2302
|
+
|
|
2303
|
+
const endpoints = apiSettingsData.endpoints || [];
|
|
2304
|
+
|
|
2305
|
+
if (endpoints.length === 0) {
|
|
2306
|
+
container.innerHTML = '<div class="empty-state">' +
|
|
2307
|
+
'<div class="empty-icon-wrapper">' +
|
|
2308
|
+
'<i data-lucide="layers"></i>' +
|
|
2309
|
+
'</div>' +
|
|
2310
|
+
'<h4>' + t('apiSettings.noEndpoints') + '</h4>' +
|
|
2311
|
+
'<p>' + t('apiSettings.noEndpointsHint') + '</p>' +
|
|
2312
|
+
'</div>';
|
|
2313
|
+
if (window.lucide) lucide.createIcons();
|
|
2314
|
+
return;
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
container.innerHTML = endpoints.map(function(endpoint) {
|
|
2318
|
+
const provider = apiSettingsData.providers.find(function(p) { return p.id === endpoint.providerId; });
|
|
2319
|
+
const providerName = provider ? provider.name : endpoint.providerId;
|
|
2320
|
+
const providerType = provider ? provider.type : 'custom';
|
|
2321
|
+
const iconClass = getProviderIconClass(providerType);
|
|
2322
|
+
const iconName = getProviderIcon(providerType);
|
|
2323
|
+
|
|
2324
|
+
const cacheEnabled = endpoint.cacheStrategy?.enabled;
|
|
2325
|
+
const cacheStatus = cacheEnabled
|
|
2326
|
+
? endpoint.cacheStrategy.ttlMinutes + ' min'
|
|
2327
|
+
: t('apiSettings.off');
|
|
2328
|
+
|
|
2329
|
+
return '<div class="api-card">' +
|
|
2330
|
+
'<div class="card-header">' +
|
|
2331
|
+
'<div class="card-title-group">' +
|
|
2332
|
+
'<div class="card-icon ' + iconClass + '">' +
|
|
2333
|
+
'<i data-lucide="' + iconName + '"></i>' +
|
|
2334
|
+
'</div>' +
|
|
2335
|
+
'<div class="card-info">' +
|
|
2336
|
+
'<h4 class="card-title">' + endpoint.name + '</h4>' +
|
|
2337
|
+
'<code class="endpoint-id">' + endpoint.id + '</code>' +
|
|
2338
|
+
'</div>' +
|
|
2339
|
+
'</div>' +
|
|
2340
|
+
'<div class="card-actions">' +
|
|
2341
|
+
'<button class="btn-icon-sm" onclick="showEditEndpointModal(\'' + endpoint.id + '\')" title="' + t('common.edit') + '">' +
|
|
2342
|
+
'<i data-lucide="pencil"></i>' +
|
|
2343
|
+
'</button>' +
|
|
2344
|
+
'<button class="btn-icon-sm text-destructive" onclick="deleteEndpoint(\'' + endpoint.id + '\')" title="' + t('common.delete') + '">' +
|
|
2345
|
+
'<i data-lucide="trash-2"></i>' +
|
|
2346
|
+
'</button>' +
|
|
2347
|
+
'</div>' +
|
|
2348
|
+
'</div>' +
|
|
2349
|
+
'<div class="card-body">' +
|
|
2350
|
+
'<div class="card-meta-grid">' +
|
|
2351
|
+
'<div class="meta-item">' +
|
|
2352
|
+
'<span class="meta-label">' + t('apiSettings.provider') + '</span>' +
|
|
2353
|
+
'<span class="meta-value">' + providerName + '</span>' +
|
|
2354
|
+
'</div>' +
|
|
2355
|
+
'<div class="meta-item">' +
|
|
2356
|
+
'<span class="meta-label">' + t('apiSettings.model') + '</span>' +
|
|
2357
|
+
'<span class="meta-value">' + endpoint.model + '</span>' +
|
|
2358
|
+
'</div>' +
|
|
2359
|
+
'<div class="meta-item">' +
|
|
2360
|
+
'<span class="meta-label">' + t('apiSettings.cache') + '</span>' +
|
|
2361
|
+
'<span class="badge ' + (cacheEnabled ? 'badge-success' : 'badge-outline') + '">' +
|
|
2362
|
+
(cacheEnabled ? '<i data-lucide="database" style="width:12px;height:12px;margin-right:4px;"></i>' : '') +
|
|
2363
|
+
cacheStatus + '</span>' +
|
|
2364
|
+
'</div>' +
|
|
2365
|
+
'</div>' +
|
|
2366
|
+
'<div class="usage-hint">' +
|
|
2367
|
+
'<i data-lucide="terminal"></i>' +
|
|
2368
|
+
'<code>ccw cli -p "..." --model ' + endpoint.id + '</code>' +
|
|
2369
|
+
'</div>' +
|
|
2370
|
+
'</div>' +
|
|
2371
|
+
'</div>';
|
|
2372
|
+
}).join('');
|
|
2373
|
+
|
|
2374
|
+
if (window.lucide) lucide.createIcons();
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
/**
|
|
2378
|
+
* Render endpoints main panel
|
|
2379
|
+
*/
|
|
2380
|
+
function renderEndpointsMainPanel() {
|
|
2381
|
+
var container = document.getElementById('provider-detail-panel');
|
|
2382
|
+
if (!container) return;
|
|
2383
|
+
|
|
2384
|
+
var endpoints = apiSettingsData.endpoints || [];
|
|
2385
|
+
|
|
2386
|
+
var html = '<div class="endpoints-main-panel">' +
|
|
2387
|
+
'<div class="panel-header">' +
|
|
2388
|
+
'<h2>' + t('apiSettings.endpoints') + '</h2>' +
|
|
2389
|
+
'<p class="panel-subtitle">' + t('apiSettings.endpointsDescription') + '</p>' +
|
|
2390
|
+
'</div>' +
|
|
2391
|
+
'<div class="endpoints-stats">' +
|
|
2392
|
+
'<div class="stat-card">' +
|
|
2393
|
+
'<div class="stat-value">' + endpoints.length + '</div>' +
|
|
2394
|
+
'<div class="stat-label">' + t('apiSettings.totalEndpoints') + '</div>' +
|
|
2395
|
+
'</div>' +
|
|
2396
|
+
'<div class="stat-card">' +
|
|
2397
|
+
'<div class="stat-value">' + endpoints.filter(function(e) { return e.cacheStrategy?.enabled; }).length + '</div>' +
|
|
2398
|
+
'<div class="stat-label">' + t('apiSettings.cachedEndpoints') + '</div>' +
|
|
2399
|
+
'</div>' +
|
|
2400
|
+
'</div>' +
|
|
2401
|
+
'</div>';
|
|
2402
|
+
|
|
2403
|
+
container.innerHTML = html;
|
|
2404
|
+
if (window.lucide) lucide.createIcons();
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
/**
|
|
2408
|
+
* Render cache main panel
|
|
2409
|
+
*/
|
|
2410
|
+
async function renderCacheMainPanel() {
|
|
2411
|
+
var container = document.getElementById('provider-detail-panel');
|
|
2412
|
+
if (!container) return;
|
|
2413
|
+
|
|
2414
|
+
// Load cache stats
|
|
2415
|
+
var stats = await loadCacheStats();
|
|
2416
|
+
if (!stats) {
|
|
2417
|
+
stats = { totalSize: 0, maxSize: 104857600, entries: 0 };
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
var globalSettings = apiSettingsData.globalCache || { enabled: false };
|
|
2421
|
+
var totalSize = stats.totalSize || 0;
|
|
2422
|
+
var maxSize = stats.maxSize || 104857600; // Default 100MB
|
|
2423
|
+
var usedMB = (totalSize / 1024 / 1024).toFixed(2);
|
|
2424
|
+
var maxMB = (maxSize / 1024 / 1024).toFixed(0);
|
|
2425
|
+
var usagePercent = maxSize > 0 ? ((totalSize / maxSize) * 100).toFixed(1) : 0;
|
|
2426
|
+
|
|
2427
|
+
var html = '<div class="cache-main-panel">' +
|
|
2428
|
+
'<div class="panel-header">' +
|
|
2429
|
+
'<h2>' + t('apiSettings.cacheSettings') + '</h2>' +
|
|
2430
|
+
'<p class="panel-subtitle">' + t('apiSettings.cacheDescription') + '</p>' +
|
|
2431
|
+
'</div>' +
|
|
2432
|
+
// Global Cache Settings
|
|
2433
|
+
'<div class="settings-section">' +
|
|
2434
|
+
'<div class="section-header">' +
|
|
2435
|
+
'<h3>' + t('apiSettings.globalCache') + '</h3>' +
|
|
2436
|
+
'<label class="toggle-switch">' +
|
|
2437
|
+
'<input type="checkbox" id="global-cache-enabled" ' + (globalSettings.enabled ? 'checked' : '') + ' onchange="updateGlobalCacheEnabled(this.checked)" />' +
|
|
2438
|
+
'<span class="toggle-track"><span class="toggle-thumb"></span></span>' +
|
|
2439
|
+
'</label>' +
|
|
2440
|
+
'</div>' +
|
|
2441
|
+
'</div>' +
|
|
2442
|
+
// Cache Statistics
|
|
2443
|
+
'<div class="settings-section">' +
|
|
2444
|
+
'<h3>' + t('apiSettings.cacheStatistics') + '</h3>' +
|
|
2445
|
+
'<div class="cache-stats-grid">' +
|
|
2446
|
+
'<div class="stat-card">' +
|
|
2447
|
+
'<div class="stat-icon"><i data-lucide="database"></i></div>' +
|
|
2448
|
+
'<div class="stat-info">' +
|
|
2449
|
+
'<div class="stat-value">' + (stats.entries || 0) + '</div>' +
|
|
2450
|
+
'<div class="stat-label">' + t('apiSettings.cachedEntries') + '</div>' +
|
|
2451
|
+
'</div>' +
|
|
2452
|
+
'</div>' +
|
|
2453
|
+
'<div class="stat-card">' +
|
|
2454
|
+
'<div class="stat-icon"><i data-lucide="hard-drive"></i></div>' +
|
|
2455
|
+
'<div class="stat-info">' +
|
|
2456
|
+
'<div class="stat-value">' + usedMB + ' MB</div>' +
|
|
2457
|
+
'<div class="stat-label">' + t('apiSettings.storageUsed') + '</div>' +
|
|
2458
|
+
'</div>' +
|
|
2459
|
+
'</div>' +
|
|
2460
|
+
'</div>' +
|
|
2461
|
+
'<div class="storage-bar-container">' +
|
|
2462
|
+
'<div class="storage-bar">' +
|
|
2463
|
+
'<div class="storage-bar-fill" style="width: ' + usagePercent + '%"></div>' +
|
|
2464
|
+
'</div>' +
|
|
2465
|
+
'<div class="storage-label">' + usedMB + ' MB / ' + maxMB + ' MB (' + usagePercent + '%)</div>' +
|
|
2466
|
+
'</div>' +
|
|
2467
|
+
'</div>' +
|
|
2468
|
+
// Cache Actions
|
|
2469
|
+
'<div class="settings-section">' +
|
|
2470
|
+
'<h3>' + t('apiSettings.cacheActions') + '</h3>' +
|
|
2471
|
+
'<button class="btn btn-destructive" onclick="clearCache()">' +
|
|
2472
|
+
'<i data-lucide="trash-2"></i> ' + t('apiSettings.clearCache') +
|
|
2473
|
+
'</button>' +
|
|
2474
|
+
'</div>' +
|
|
2475
|
+
'</div>';
|
|
2476
|
+
|
|
2477
|
+
container.innerHTML = html;
|
|
2478
|
+
if (window.lucide) lucide.createIcons();
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
/**
|
|
2482
|
+
* Render cache settings panel
|
|
2483
|
+
*/
|
|
2484
|
+
function renderCacheSettings(stats) {
|
|
2485
|
+
const container = document.getElementById('cache-settings-panel');
|
|
2486
|
+
if (!container) return;
|
|
2487
|
+
|
|
2488
|
+
const globalSettings = apiSettingsData.globalCache || { enabled: false };
|
|
2489
|
+
const totalSize = stats.totalSize || 0;
|
|
2490
|
+
const maxSize = stats.maxSize || 104857600; // Default 100MB
|
|
2491
|
+
const usedMB = (totalSize / 1024 / 1024).toFixed(2);
|
|
2492
|
+
const maxMB = (maxSize / 1024 / 1024).toFixed(0);
|
|
2493
|
+
const usagePercent = maxSize > 0 ? ((totalSize / maxSize) * 100).toFixed(1) : 0;
|
|
2494
|
+
|
|
2495
|
+
container.innerHTML = '<div class="cache-panel">' +
|
|
2496
|
+
// Cache Header
|
|
2497
|
+
'<div class="cache-header">' +
|
|
2498
|
+
'<div class="section-title-group">' +
|
|
2499
|
+
'<h3>' + t('apiSettings.cacheSettings') + '</h3>' +
|
|
2500
|
+
'</div>' +
|
|
2501
|
+
'<label class="toggle-switch">' +
|
|
2502
|
+
'<input type="checkbox" id="global-cache-enabled" ' + (globalSettings.enabled ? 'checked' : '') + ' onchange="toggleGlobalCache()" />' +
|
|
2503
|
+
'<span class="toggle-track"><span class="toggle-thumb"></span></span>' +
|
|
2504
|
+
'<span class="toggle-label">' + t('apiSettings.enableGlobalCaching') + '</span>' +
|
|
2505
|
+
'</label>' +
|
|
2506
|
+
'</div>' +
|
|
2507
|
+
// Cache Content
|
|
2508
|
+
'<div class="cache-content">' +
|
|
2509
|
+
// Visual Bar
|
|
2510
|
+
'<div class="cache-visual">' +
|
|
2511
|
+
'<div class="cache-bars">' +
|
|
2512
|
+
'<div class="cache-bar-fill" style="width: ' + usagePercent + '%"></div>' +
|
|
2513
|
+
'</div>' +
|
|
2514
|
+
'<div class="cache-legend">' +
|
|
2515
|
+
'<span>' + usedMB + ' MB ' + t('apiSettings.used') + '</span>' +
|
|
2516
|
+
'<span>' + maxMB + ' MB ' + t('apiSettings.total') + '</span>' +
|
|
2517
|
+
'</div>' +
|
|
2518
|
+
'</div>' +
|
|
2519
|
+
// Stats Grid
|
|
2520
|
+
'<div class="stat-grid">' +
|
|
2521
|
+
'<div class="stat-card">' +
|
|
2522
|
+
'<span class="stat-value">' + usagePercent + '%</span>' +
|
|
2523
|
+
'<span class="stat-desc">' + t('apiSettings.cacheUsage') + '</span>' +
|
|
2524
|
+
'</div>' +
|
|
2525
|
+
'<div class="stat-card">' +
|
|
2526
|
+
'<span class="stat-value">' + (stats.entries || 0) + '</span>' +
|
|
2527
|
+
'<span class="stat-desc">' + t('apiSettings.cacheEntries') + '</span>' +
|
|
2528
|
+
'</div>' +
|
|
2529
|
+
'<div class="stat-card">' +
|
|
2530
|
+
'<span class="stat-value">' + usedMB + ' MB</span>' +
|
|
2531
|
+
'<span class="stat-desc">' + t('apiSettings.cacheSize') + '</span>' +
|
|
2532
|
+
'</div>' +
|
|
2533
|
+
'</div>' +
|
|
2534
|
+
// Clear Button
|
|
2535
|
+
'<button class="btn btn-secondary" onclick="clearCache()" style="align-self: flex-start;">' +
|
|
2536
|
+
'<i data-lucide="trash-2"></i> ' + t('apiSettings.clearCache') +
|
|
2537
|
+
'</button>' +
|
|
2538
|
+
'</div>' +
|
|
2539
|
+
'</div>';
|
|
2540
|
+
|
|
2541
|
+
if (window.lucide) lucide.createIcons();
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
// ========== Multi-Key Management ==========
|
|
2545
|
+
|
|
2546
|
+
/**
|
|
2547
|
+
* Generate unique ID for API keys
|
|
2548
|
+
*/
|
|
2549
|
+
function generateKeyId() {
|
|
2550
|
+
return 'key-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
// ========== Embedding Pool Management ==========
|
|
2554
|
+
|
|
2555
|
+
/**
|
|
2556
|
+
* Render embedding pool sidebar summary
|
|
2557
|
+
*/
|
|
2558
|
+
function renderEmbeddingPoolSidebar() {
|
|
2559
|
+
if (!embeddingPoolConfig) {
|
|
2560
|
+
return '<div class="embedding-pool-sidebar-info" style="padding: 1rem;">' +
|
|
2561
|
+
'<div style="text-align: center; color: hsl(var(--muted-foreground)); font-size: 0.875rem;">' +
|
|
2562
|
+
'<p>' + t('apiSettings.embeddingPoolDesc') + '</p>' +
|
|
2563
|
+
'</div>' +
|
|
2564
|
+
'</div>';
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
const enabled = embeddingPoolConfig.enabled || false;
|
|
2568
|
+
const targetModel = embeddingPoolConfig.targetModel || '';
|
|
2569
|
+
const strategy = embeddingPoolConfig.strategy || 'round_robin';
|
|
2570
|
+
const excludedIds = embeddingPoolConfig.excludedProviderIds || [];
|
|
2571
|
+
|
|
2572
|
+
// Count total providers/keys
|
|
2573
|
+
let totalProviders = embeddingPoolDiscoveredProviders.length;
|
|
2574
|
+
let totalKeys = 0;
|
|
2575
|
+
let activeProviders = 0;
|
|
2576
|
+
|
|
2577
|
+
embeddingPoolDiscoveredProviders.forEach(function(p) {
|
|
2578
|
+
totalKeys += p.apiKeys?.length || 1;
|
|
2579
|
+
if (excludedIds.indexOf(p.providerId) === -1) {
|
|
2580
|
+
activeProviders++;
|
|
2581
|
+
}
|
|
2582
|
+
});
|
|
2583
|
+
|
|
2584
|
+
const strategyLabels = {
|
|
2585
|
+
'round_robin': t('codexlens.strategyRoundRobin') || 'Round Robin',
|
|
2586
|
+
'latency_aware': t('codexlens.strategyLatency') || 'Latency-Aware',
|
|
2587
|
+
'weighted_random': t('codexlens.strategyWeighted') || 'Weighted Random'
|
|
2588
|
+
};
|
|
2589
|
+
|
|
2590
|
+
return '<div class="embedding-pool-sidebar-summary" style="padding: 1rem; display: flex; flex-direction: column; gap: 1rem;">' +
|
|
2591
|
+
'<div style="padding: 1rem; background: hsl(var(--muted) / 0.3); border-radius: 0.5rem;">' +
|
|
2592
|
+
'<h4 style="margin: 0 0 0.75rem 0; font-size: 0.875rem; font-weight: 600; color: hsl(var(--foreground));">' +
|
|
2593
|
+
t('apiSettings.embeddingPool') +
|
|
2594
|
+
'</h4>' +
|
|
2595
|
+
'<div style="display: flex; flex-direction: column; gap: 0.5rem; font-size: 0.75rem; color: hsl(var(--muted-foreground));">' +
|
|
2596
|
+
'<div style="display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem 0; border-bottom: 1px solid hsl(var(--border));">' +
|
|
2597
|
+
'<i data-lucide="' + (enabled ? 'check-circle' : 'x-circle') + '" style="width: 14px; height: 14px; color: ' + (enabled ? 'hsl(var(--success))' : 'hsl(var(--muted-foreground))') + ';"></i>' +
|
|
2598
|
+
'<span>' + (enabled ? (t('common.enabled') || 'Enabled') : (t('common.disabled') || 'Disabled')) + '</span>' +
|
|
2599
|
+
'</div>' +
|
|
2600
|
+
(enabled && targetModel ?
|
|
2601
|
+
'<div style="display: flex; flex-direction: column; gap: 0.25rem; padding: 0.375rem 0; border-bottom: 1px solid hsl(var(--border));">' +
|
|
2602
|
+
'<span style="font-weight: 500; color: hsl(var(--foreground));">' + t('apiSettings.targetModel') + '</span>' +
|
|
2603
|
+
'<code style="font-size: 0.6875rem; color: hsl(var(--primary)); word-break: break-all;">' + targetModel + '</code>' +
|
|
2604
|
+
'</div>' : '') +
|
|
2605
|
+
(enabled ?
|
|
2606
|
+
'<div style="display: flex; flex-direction: column; gap: 0.25rem; padding: 0.375rem 0; border-bottom: 1px solid hsl(var(--border));">' +
|
|
2607
|
+
'<span style="font-weight: 500; color: hsl(var(--foreground));">' + t('apiSettings.strategy') + '</span>' +
|
|
2608
|
+
'<span>' + (strategyLabels[strategy] || strategy) + '</span>' +
|
|
2609
|
+
'</div>' : '') +
|
|
2610
|
+
(enabled && totalProviders > 0 ?
|
|
2611
|
+
'<div style="display: flex; flex-direction: column; gap: 0.25rem; padding: 0.375rem 0;">' +
|
|
2612
|
+
'<span style="font-weight: 500; color: hsl(var(--foreground));">' + t('apiSettings.discoveredProviders') + '</span>' +
|
|
2613
|
+
'<span>' + activeProviders + ' / ' + totalProviders + ' providers (' + totalKeys + ' keys)</span>' +
|
|
2614
|
+
'</div>' : '') +
|
|
2615
|
+
'</div>' +
|
|
2616
|
+
'</div>' +
|
|
2617
|
+
'</div>';
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
|
|
2621
|
+
/**
|
|
2622
|
+
* Render embedding pool main panel
|
|
2623
|
+
*/
|
|
2624
|
+
async function renderEmbeddingPoolMainPanel() {
|
|
2625
|
+
var container = document.getElementById('provider-detail-panel');
|
|
2626
|
+
if (!container) return;
|
|
2627
|
+
|
|
2628
|
+
// Load embedding pool config if not already loaded
|
|
2629
|
+
if (!embeddingPoolConfig) {
|
|
2630
|
+
await loadEmbeddingPoolConfig();
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
const enabled = embeddingPoolConfig?.enabled || false;
|
|
2634
|
+
const targetModel = embeddingPoolConfig?.targetModel || '';
|
|
2635
|
+
const strategy = embeddingPoolConfig?.strategy || 'round_robin';
|
|
2636
|
+
const defaultCooldown = embeddingPoolConfig?.defaultCooldown || 60;
|
|
2637
|
+
const defaultMaxConcurrentPerKey = embeddingPoolConfig?.defaultMaxConcurrentPerKey || 4;
|
|
2638
|
+
|
|
2639
|
+
// Build model dropdown options
|
|
2640
|
+
let modelOptionsHtml = '<option value="">' + t('apiSettings.selectTargetModel') + '</option>';
|
|
2641
|
+
embeddingPoolAvailableModels.forEach(function(model) {
|
|
2642
|
+
const providerCount = model.providers.length;
|
|
2643
|
+
const selected = model.modelId === targetModel ? ' selected' : '';
|
|
2644
|
+
modelOptionsHtml += '<option value="' + model.modelId + '"' + selected + '>' +
|
|
2645
|
+
model.modelName + ' (' + providerCount + ' providers)' +
|
|
2646
|
+
'</option>';
|
|
2647
|
+
});
|
|
2648
|
+
|
|
2649
|
+
var html = '<div class="embedding-pool-main-panel">' +
|
|
2650
|
+
'<div class="panel-header">' +
|
|
2651
|
+
'<h2><i data-lucide="repeat"></i> ' + t('apiSettings.embeddingPool') + '</h2>' +
|
|
2652
|
+
'<p class="panel-subtitle">' + t('apiSettings.embeddingPoolDesc') + '</p>' +
|
|
2653
|
+
'</div>' +
|
|
2654
|
+
|
|
2655
|
+
// Enable/Disable Toggle Card
|
|
2656
|
+
'<div class="settings-section" style="padding: 1.25rem; background: hsl(var(--muted) / 0.3); border-radius: 0.75rem;">' +
|
|
2657
|
+
'<div class="section-header" style="border: none; padding: 0;">' +
|
|
2658
|
+
'<div style="display: flex; align-items: center; gap: 0.5rem;">' +
|
|
2659
|
+
'<i data-lucide="power" style="width: 1rem; height: 1rem; color: hsl(var(--primary));"></i>' +
|
|
2660
|
+
'<h3 style="margin: 0;">' + t('apiSettings.poolEnabled') + '</h3>' +
|
|
2661
|
+
'</div>' +
|
|
2662
|
+
'<label class="toggle-switch">' +
|
|
2663
|
+
'<input type="checkbox" id="embedding-pool-enabled" ' + (enabled ? 'checked' : '') + ' onchange="onEmbeddingPoolEnabledChange(this.checked)" />' +
|
|
2664
|
+
'<span class="toggle-track"><span class="toggle-thumb"></span></span>' +
|
|
2665
|
+
'</label>' +
|
|
2666
|
+
'</div>' +
|
|
2667
|
+
'</div>' +
|
|
2668
|
+
|
|
2669
|
+
// Configuration Form Card
|
|
2670
|
+
'<div class="settings-section" id="embedding-pool-config" style="' + (enabled ? '' : 'display: none;') + '">' +
|
|
2671
|
+
|
|
2672
|
+
// Model and Strategy in Grid
|
|
2673
|
+
'<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1rem;">' +
|
|
2674
|
+
'<div class="form-group" style="margin: 0;">' +
|
|
2675
|
+
'<label for="embedding-pool-target-model">' + t('apiSettings.targetModel') + '</label>' +
|
|
2676
|
+
'<select id="embedding-pool-target-model" class="cli-input" onchange="onTargetModelChange(this.value)">' +
|
|
2677
|
+
modelOptionsHtml +
|
|
2678
|
+
'</select>' +
|
|
2679
|
+
'</div>' +
|
|
2680
|
+
'<div class="form-group" style="margin: 0;">' +
|
|
2681
|
+
'<label for="embedding-pool-strategy">' + t('apiSettings.strategy') + '</label>' +
|
|
2682
|
+
'<select id="embedding-pool-strategy" class="cli-input">' +
|
|
2683
|
+
'<option value="round_robin"' + (strategy === 'round_robin' ? ' selected' : '') + '>Round Robin</option>' +
|
|
2684
|
+
'<option value="latency_aware"' + (strategy === 'latency_aware' ? ' selected' : '') + '>Latency Aware</option>' +
|
|
2685
|
+
'<option value="weighted_random"' + (strategy === 'weighted_random' ? ' selected' : '') + '>Weighted Random</option>' +
|
|
2686
|
+
'</select>' +
|
|
2687
|
+
'</div>' +
|
|
2688
|
+
'</div>' +
|
|
2689
|
+
|
|
2690
|
+
// Cooldown and Concurrent in Grid
|
|
2691
|
+
'<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">' +
|
|
2692
|
+
'<div class="form-group" style="margin: 0;">' +
|
|
2693
|
+
'<label for="embedding-pool-cooldown">' + t('apiSettings.defaultCooldown') + ' (s)</label>' +
|
|
2694
|
+
'<input type="number" id="embedding-pool-cooldown" class="cli-input" value="' + defaultCooldown + '" min="1" />' +
|
|
2695
|
+
'</div>' +
|
|
2696
|
+
'<div class="form-group" style="margin: 0;">' +
|
|
2697
|
+
'<label for="embedding-pool-concurrent">' + t('apiSettings.defaultConcurrent') + '</label>' +
|
|
2698
|
+
'<input type="number" id="embedding-pool-concurrent" class="cli-input" value="' + defaultMaxConcurrentPerKey + '" min="1" />' +
|
|
2699
|
+
'</div>' +
|
|
2700
|
+
'</div>' +
|
|
2701
|
+
|
|
2702
|
+
// Discovered Providers Section
|
|
2703
|
+
'<div id="discovered-providers-section" style="margin-top: 1.5rem;"></div>' +
|
|
2704
|
+
|
|
2705
|
+
'<div class="form-actions">' +
|
|
2706
|
+
'<button class="btn btn-primary" onclick="saveEmbeddingPoolConfig()">' +
|
|
2707
|
+
'<i data-lucide="save"></i> ' + t('common.save') +
|
|
2708
|
+
'</button>' +
|
|
2709
|
+
'</div>' +
|
|
2710
|
+
'</div>' +
|
|
2711
|
+
'</div>';
|
|
2712
|
+
|
|
2713
|
+
container.innerHTML = html;
|
|
2714
|
+
if (window.lucide) lucide.createIcons();
|
|
2715
|
+
|
|
2716
|
+
// Render discovered providers if we have a target model
|
|
2717
|
+
if (enabled && targetModel) {
|
|
2718
|
+
renderDiscoveredProviders();
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
/**
|
|
2723
|
+
* Handle embedding pool enabled/disabled toggle
|
|
2724
|
+
*/
|
|
2725
|
+
function onEmbeddingPoolEnabledChange(enabled) {
|
|
2726
|
+
const configSection = document.getElementById('embedding-pool-config');
|
|
2727
|
+
if (configSection) {
|
|
2728
|
+
configSection.style.display = enabled ? '' : 'none';
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
/**
|
|
2733
|
+
* Handle target model selection change
|
|
2734
|
+
*/
|
|
2735
|
+
async function onTargetModelChange(modelId) {
|
|
2736
|
+
if (!modelId) {
|
|
2737
|
+
embeddingPoolDiscoveredProviders = [];
|
|
2738
|
+
renderDiscoveredProviders();
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2742
|
+
// Discover providers for this model
|
|
2743
|
+
await discoverProvidersForTargetModel(modelId);
|
|
2744
|
+
renderDiscoveredProviders();
|
|
2745
|
+
|
|
2746
|
+
// Update sidebar summary
|
|
2747
|
+
const sidebarContainer = document.querySelector('.api-settings-sidebar');
|
|
2748
|
+
if (sidebarContainer) {
|
|
2749
|
+
const contentArea = sidebarContainer.querySelector('.provider-list, .endpoints-list, .embedding-pool-sidebar-info, .embedding-pool-sidebar-summary, .cache-sidebar-info');
|
|
2750
|
+
if (contentArea && contentArea.parentElement) {
|
|
2751
|
+
contentArea.parentElement.innerHTML = renderEmbeddingPoolSidebar();
|
|
2752
|
+
if (window.lucide) lucide.createIcons();
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
/**
|
|
2758
|
+
* Render discovered providers list
|
|
2759
|
+
*/
|
|
2760
|
+
function renderDiscoveredProviders() {
|
|
2761
|
+
const container = document.getElementById('discovered-providers-section');
|
|
2762
|
+
if (!container) return;
|
|
2763
|
+
|
|
2764
|
+
if (embeddingPoolDiscoveredProviders.length === 0) {
|
|
2765
|
+
container.innerHTML = '<div class="info-message">' +
|
|
2766
|
+
'<i data-lucide="info"></i> ' + t('apiSettings.noProvidersFound') +
|
|
2767
|
+
'</div>';
|
|
2768
|
+
if (window.lucide) lucide.createIcons();
|
|
2769
|
+
return;
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
const excludedIds = embeddingPoolConfig?.excludedProviderIds || [];
|
|
2773
|
+
let totalProviders = 0;
|
|
2774
|
+
let totalKeys = 0;
|
|
2775
|
+
|
|
2776
|
+
embeddingPoolDiscoveredProviders.forEach(function(p) {
|
|
2777
|
+
totalProviders++;
|
|
2778
|
+
totalKeys += p.apiKeys?.length || 1;
|
|
2779
|
+
});
|
|
2780
|
+
|
|
2781
|
+
let providersHtml = '<div>' +
|
|
2782
|
+
'<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem;">' +
|
|
2783
|
+
'<h4 style="margin: 0; font-size: 0.9375rem; font-weight: 600;">' + t('apiSettings.discoveredProviders') + '</h4>' +
|
|
2784
|
+
'<span style="font-size: 0.75rem; color: hsl(var(--muted-foreground)); padding: 0.25rem 0.625rem; background: hsl(var(--muted) / 0.5); border-radius: 9999px;">' +
|
|
2785
|
+
totalProviders + ' providers, ' + totalKeys + ' keys' +
|
|
2786
|
+
'</span>' +
|
|
2787
|
+
'</div>' +
|
|
2788
|
+
'<div style="display: flex; flex-direction: column; gap: 0.75rem;">';
|
|
2789
|
+
|
|
2790
|
+
embeddingPoolDiscoveredProviders.forEach(function(provider, index) {
|
|
2791
|
+
const isExcluded = excludedIds.indexOf(provider.providerId) > -1;
|
|
2792
|
+
const keyCount = provider.apiKeys?.length || 1;
|
|
2793
|
+
|
|
2794
|
+
// Get provider icon
|
|
2795
|
+
let providerIcon = 'server';
|
|
2796
|
+
if (provider.providerName.toLowerCase().includes('openai')) providerIcon = 'brain';
|
|
2797
|
+
else if (provider.providerName.toLowerCase().includes('modelscope')) providerIcon = 'cpu';
|
|
2798
|
+
else if (provider.providerName.toLowerCase().includes('azure')) providerIcon = 'cloud';
|
|
2799
|
+
|
|
2800
|
+
providersHtml += '<div style="border: 1px solid hsl(var(--border)); border-radius: 0.5rem; padding: 1rem; ' +
|
|
2801
|
+
(isExcluded ? 'opacity: 0.5; background: hsl(var(--muted) / 0.3);' : 'background: hsl(var(--card));') + '">' +
|
|
2802
|
+
'<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.75rem;">' +
|
|
2803
|
+
'<div style="display: flex; align-items: center; gap: 0.75rem;">' +
|
|
2804
|
+
'<div style="width: 2rem; height: 2rem; border-radius: 0.375rem; background: ' + (isExcluded ? 'hsl(var(--muted))' : 'hsl(var(--success) / 0.1)') + '; color: ' + (isExcluded ? 'hsl(var(--muted-foreground))' : 'hsl(var(--success))') + '; display: flex; align-items: center; justify-content: center;">' +
|
|
2805
|
+
'<i data-lucide="' + providerIcon + '" style="width: 1.125rem; height: 1.125rem;"></i>' +
|
|
2806
|
+
'</div>' +
|
|
2807
|
+
'<div>' +
|
|
2808
|
+
'<div style="font-weight: 500; font-size: 0.875rem; color: hsl(var(--foreground));">' + escapeHtml(provider.providerName) + '</div>' +
|
|
2809
|
+
'<div style="font-size: 0.75rem; color: hsl(var(--muted-foreground));">' + provider.modelName + ' · ' + keyCount + ' key' + (keyCount > 1 ? 's' : '') + '</div>' +
|
|
2810
|
+
'</div>' +
|
|
2811
|
+
'</div>' +
|
|
2812
|
+
'<button class="btn btn-sm ' + (isExcluded ? 'btn-primary' : 'btn-outline') + '" onclick="toggleProviderExclusion(\'' + provider.providerId + '\')" style="flex-shrink: 0;">' +
|
|
2813
|
+
'<i data-lucide="' + (isExcluded ? 'plus' : 'x') + '" style="width: 0.875rem; height: 0.875rem;"></i> ' +
|
|
2814
|
+
(isExcluded ? t('common.include') : t('apiSettings.excludeProvider')) +
|
|
2815
|
+
'</button>' +
|
|
2816
|
+
'</div>' +
|
|
2817
|
+
'</div>';
|
|
2818
|
+
});
|
|
2819
|
+
|
|
2820
|
+
providersHtml += '</div>';
|
|
2821
|
+
container.innerHTML = providersHtml;
|
|
2822
|
+
if (window.lucide) lucide.createIcons();
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
/**
|
|
2826
|
+
* Render API keys section
|
|
2827
|
+
*/
|
|
2828
|
+
function renderApiKeysSection(provider) {
|
|
2829
|
+
const keys = provider.apiKeys || [];
|
|
2830
|
+
const hasMultipleKeys = keys.length > 0;
|
|
2831
|
+
|
|
2832
|
+
let keysHtml = '';
|
|
2833
|
+
if (hasMultipleKeys) {
|
|
2834
|
+
keysHtml = keys.map(function(key, index) {
|
|
2835
|
+
return '<div class="api-key-item" data-key-id="' + key.id + '">' +
|
|
2836
|
+
'<input type="text" class="cli-input key-label" ' +
|
|
2837
|
+
'value="' + (key.label || '') + '" ' +
|
|
2838
|
+
'placeholder="' + t('apiSettings.keyLabel') + '" ' +
|
|
2839
|
+
'onchange="updateApiKeyField(\'' + provider.id + '\', \'' + key.id + '\', \'label\', this.value)">' +
|
|
2840
|
+
'<div class="key-value-wrapper" style="display: flex; gap: 0.5rem;">' +
|
|
2841
|
+
'<input type="password" class="cli-input key-value" ' +
|
|
2842
|
+
'value="' + key.key + '" ' +
|
|
2843
|
+
'placeholder="' + t('apiSettings.keyValue') + '" ' +
|
|
2844
|
+
'onchange="updateApiKeyField(\'' + provider.id + '\', \'' + key.id + '\', \'key\', this.value)">' +
|
|
2845
|
+
'<button type="button" class="btn-icon" onclick="toggleKeyVisibility(this)">👁️</button>' +
|
|
2846
|
+
'</div>' +
|
|
2847
|
+
'<input type="number" class="cli-input key-weight" ' +
|
|
2848
|
+
'value="' + (key.weight || 1) + '" min="1" max="100" ' +
|
|
2849
|
+
'placeholder="' + t('apiSettings.keyWeight') + '" ' +
|
|
2850
|
+
'onchange="updateApiKeyField(\'' + provider.id + '\', \'' + key.id + '\', \'weight\', parseInt(this.value))">' +
|
|
2851
|
+
'<div class="key-status">' +
|
|
2852
|
+
'<span class="key-status-indicator ' + (key.healthStatus || 'unknown') + '"></span>' +
|
|
2853
|
+
'<span class="key-status-text">' + t('apiSettings.' + (key.healthStatus || 'unknown')) + '</span>' +
|
|
2854
|
+
'</div>' +
|
|
2855
|
+
'<div class="api-key-actions">' +
|
|
2856
|
+
'<button type="button" class="test-key-btn" onclick="testApiKey(\'' + provider.id + '\', \'' + key.id + '\')">' +
|
|
2857
|
+
t('apiSettings.testKey') +
|
|
2858
|
+
'</button>' +
|
|
2859
|
+
'<button type="button" class="btn-danger btn-sm" onclick="removeApiKey(\'' + provider.id + '\', \'' + key.id + '\')">' +
|
|
2860
|
+
t('apiSettings.removeKey') +
|
|
2861
|
+
'</button>' +
|
|
2862
|
+
'</div>' +
|
|
2863
|
+
'</div>';
|
|
2864
|
+
}).join('');
|
|
2865
|
+
} else {
|
|
2866
|
+
keysHtml = '<div class="no-keys-message">' + t('apiSettings.noKeys') + '</div>';
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
return '<div class="api-keys-section">' +
|
|
2870
|
+
'<div class="api-keys-header">' +
|
|
2871
|
+
'<h4>' + t('apiSettings.apiKeys') + '</h4>' +
|
|
2872
|
+
'<button type="button" class="add-key-btn btn-secondary" onclick="addApiKey(\'' + provider.id + '\')">' +
|
|
2873
|
+
'+ ' + t('apiSettings.addKey') +
|
|
2874
|
+
'</button>' +
|
|
2875
|
+
'</div>' +
|
|
2876
|
+
'<div class="api-key-list" id="api-key-list-' + provider.id + '">' +
|
|
2877
|
+
keysHtml +
|
|
2878
|
+
'</div>' +
|
|
2879
|
+
'</div>';
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
/**
|
|
2883
|
+
* Render routing strategy section
|
|
2884
|
+
*/
|
|
2885
|
+
function renderRoutingSection(provider) {
|
|
2886
|
+
const strategy = provider.routingStrategy || 'simple-shuffle';
|
|
2887
|
+
|
|
2888
|
+
return '<div class="routing-section">' +
|
|
2889
|
+
'<label>' + t('apiSettings.routingStrategy') + '</label>' +
|
|
2890
|
+
'<select class="cli-input" onchange="updateProviderRouting(\'' + provider.id + '\', this.value)">' +
|
|
2891
|
+
'<option value="simple-shuffle"' + (strategy === 'simple-shuffle' ? ' selected' : '') + '>' + t('apiSettings.simpleShuffleRouting') + '</option>' +
|
|
2892
|
+
'<option value="weighted"' + (strategy === 'weighted' ? ' selected' : '') + '>' + t('apiSettings.weightedRouting') + '</option>' +
|
|
2893
|
+
'<option value="latency-based"' + (strategy === 'latency-based' ? ' selected' : '') + '>' + t('apiSettings.latencyRouting') + '</option>' +
|
|
2894
|
+
'<option value="cost-based"' + (strategy === 'cost-based' ? ' selected' : '') + '>' + t('apiSettings.costRouting') + '</option>' +
|
|
2895
|
+
'<option value="least-busy"' + (strategy === 'least-busy' ? ' selected' : '') + '>' + t('apiSettings.leastBusyRouting') + '</option>' +
|
|
2896
|
+
'</select>' +
|
|
2897
|
+
'<div class="routing-hint">' + t('apiSettings.routingHint') + '</div>' +
|
|
2898
|
+
'</div>';
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2901
|
+
/**
|
|
2902
|
+
* Render health check section
|
|
2903
|
+
*/
|
|
2904
|
+
function renderHealthCheckSection(provider) {
|
|
2905
|
+
const health = provider.healthCheck || { enabled: false, intervalSeconds: 300, cooldownSeconds: 5, failureThreshold: 3 };
|
|
2906
|
+
|
|
2907
|
+
return '<div class="health-check-section">' +
|
|
2908
|
+
'<div class="health-check-header">' +
|
|
2909
|
+
'<h5>' + t('apiSettings.healthCheck') + '</h5>' +
|
|
2910
|
+
'<label class="toggle-switch">' +
|
|
2911
|
+
'<input type="checkbox"' + (health.enabled ? ' checked' : '') + ' ' +
|
|
2912
|
+
'onchange="updateHealthCheckEnabled(\'' + provider.id + '\', this.checked)">' +
|
|
2913
|
+
'<span class="toggle-slider"></span>' +
|
|
2914
|
+
'</label>' +
|
|
2915
|
+
'</div>' +
|
|
2916
|
+
'<div class="health-check-grid" style="' + (health.enabled ? '' : 'opacity: 0.5; pointer-events: none;') + '">' +
|
|
2917
|
+
'<div class="health-check-field">' +
|
|
2918
|
+
'<label>' + t('apiSettings.healthInterval') + '</label>' +
|
|
2919
|
+
'<input type="number" class="cli-input" value="' + health.intervalSeconds + '" min="60" max="3600" ' +
|
|
2920
|
+
'onchange="updateHealthCheckField(\'' + provider.id + '\', \'intervalSeconds\', parseInt(this.value))">' +
|
|
2921
|
+
'</div>' +
|
|
2922
|
+
'<div class="health-check-field">' +
|
|
2923
|
+
'<label>' + t('apiSettings.healthCooldown') + '</label>' +
|
|
2924
|
+
'<input type="number" class="cli-input" value="' + health.cooldownSeconds + '" min="1" max="60" ' +
|
|
2925
|
+
'onchange="updateHealthCheckField(\'' + provider.id + '\', \'cooldownSeconds\', parseInt(this.value))">' +
|
|
2926
|
+
'</div>' +
|
|
2927
|
+
'<div class="health-check-field">' +
|
|
2928
|
+
'<label>' + t('apiSettings.failureThreshold') + '</label>' +
|
|
2929
|
+
'<input type="number" class="cli-input" value="' + health.failureThreshold + '" min="1" max="10" ' +
|
|
2930
|
+
'onchange="updateHealthCheckField(\'' + provider.id + '\', \'failureThreshold\', parseInt(this.value))">' +
|
|
2931
|
+
'</div>' +
|
|
2932
|
+
'</div>' +
|
|
2933
|
+
'</div>';
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
/**
|
|
2937
|
+
* Show multi-key settings modal
|
|
2938
|
+
*/
|
|
2939
|
+
function showMultiKeyModal(providerId) {
|
|
2940
|
+
const provider = apiSettingsData.providers.find(function(p) { return p.id === providerId; });
|
|
2941
|
+
if (!provider) return;
|
|
2942
|
+
|
|
2943
|
+
const modalHtml = '<div class="modal-overlay" id="multi-key-modal">' +
|
|
2944
|
+
'<div class="modal-content" style="max-width: 700px; max-height: 85vh; overflow-y: auto;">' +
|
|
2945
|
+
'<div class="modal-header">' +
|
|
2946
|
+
'<h3>' + t('apiSettings.multiKeySettings') + '</h3>' +
|
|
2947
|
+
'<button class="modal-close" onclick="closeMultiKeyModal()">×</button>' +
|
|
2948
|
+
'</div>' +
|
|
2949
|
+
'<div class="modal-body">' +
|
|
2950
|
+
renderApiKeysSection(provider) +
|
|
2951
|
+
renderRoutingSection(provider) +
|
|
2952
|
+
renderHealthCheckSection(provider) +
|
|
2953
|
+
'</div>' +
|
|
2954
|
+
'<div class="modal-actions">' +
|
|
2955
|
+
'<button type="button" class="btn-primary" onclick="closeMultiKeyModal()"><i data-lucide="check"></i> ' + t('common.close') + '</button>' +
|
|
2956
|
+
'</div>' +
|
|
2957
|
+
'</div>' +
|
|
2958
|
+
'</div>';
|
|
2959
|
+
|
|
2960
|
+
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
|
2961
|
+
if (window.lucide) lucide.createIcons();
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
/**
|
|
2965
|
+
* Close multi-key settings modal
|
|
2966
|
+
*/
|
|
2967
|
+
function closeMultiKeyModal() {
|
|
2968
|
+
const modal = document.getElementById('multi-key-modal');
|
|
2969
|
+
if (modal) modal.remove();
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2972
|
+
/**
|
|
2973
|
+
* Refresh multi-key modal content
|
|
2974
|
+
*/
|
|
2975
|
+
function refreshMultiKeyModal(providerId) {
|
|
2976
|
+
const modal = document.getElementById('multi-key-modal');
|
|
2977
|
+
if (!modal) return;
|
|
2978
|
+
|
|
2979
|
+
const provider = apiSettingsData.providers.find(function(p) { return p.id === providerId; });
|
|
2980
|
+
if (!provider) return;
|
|
2981
|
+
|
|
2982
|
+
const modalBody = modal.querySelector('.modal-body');
|
|
2983
|
+
if (modalBody) {
|
|
2984
|
+
modalBody.innerHTML =
|
|
2985
|
+
renderApiKeysSection(provider) +
|
|
2986
|
+
renderRoutingSection(provider) +
|
|
2987
|
+
renderHealthCheckSection(provider);
|
|
2988
|
+
if (window.lucide) lucide.createIcons();
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
/**
|
|
2993
|
+
* Add API key to provider
|
|
2994
|
+
*/
|
|
2995
|
+
function addApiKey(providerId) {
|
|
2996
|
+
const newKey = {
|
|
2997
|
+
id: generateKeyId(),
|
|
2998
|
+
key: '',
|
|
2999
|
+
label: '',
|
|
3000
|
+
weight: 1,
|
|
3001
|
+
enabled: true,
|
|
3002
|
+
healthStatus: 'unknown'
|
|
3003
|
+
};
|
|
3004
|
+
|
|
3005
|
+
fetch('/api/litellm-api/providers/' + providerId)
|
|
3006
|
+
.then(function(res) { return res.json(); })
|
|
3007
|
+
.then(function(provider) {
|
|
3008
|
+
const apiKeys = provider.apiKeys || [];
|
|
3009
|
+
apiKeys.push(newKey);
|
|
3010
|
+
return fetch('/api/litellm-api/providers/' + providerId, {
|
|
3011
|
+
method: 'PUT',
|
|
3012
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3013
|
+
body: JSON.stringify({ apiKeys: apiKeys })
|
|
3014
|
+
});
|
|
3015
|
+
})
|
|
3016
|
+
.then(function() {
|
|
3017
|
+
loadApiSettings().then(function() {
|
|
3018
|
+
refreshMultiKeyModal(providerId);
|
|
3019
|
+
});
|
|
3020
|
+
})
|
|
3021
|
+
.catch(function(err) {
|
|
3022
|
+
console.error('Failed to add API key:', err);
|
|
3023
|
+
});
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
/**
|
|
3027
|
+
* Remove API key from provider
|
|
3028
|
+
*/
|
|
3029
|
+
function removeApiKey(providerId, keyId) {
|
|
3030
|
+
if (!confirm(t('common.confirmDelete'))) return;
|
|
3031
|
+
|
|
3032
|
+
fetch('/api/litellm-api/providers/' + providerId)
|
|
3033
|
+
.then(function(res) { return res.json(); })
|
|
3034
|
+
.then(function(provider) {
|
|
3035
|
+
const apiKeys = (provider.apiKeys || []).filter(function(k) { return k.id !== keyId; });
|
|
3036
|
+
return fetch('/api/litellm-api/providers/' + providerId, {
|
|
3037
|
+
method: 'PUT',
|
|
3038
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3039
|
+
body: JSON.stringify({ apiKeys: apiKeys })
|
|
3040
|
+
});
|
|
3041
|
+
})
|
|
3042
|
+
.then(function() {
|
|
3043
|
+
loadApiSettings().then(function() {
|
|
3044
|
+
refreshMultiKeyModal(providerId);
|
|
3045
|
+
});
|
|
3046
|
+
})
|
|
3047
|
+
.catch(function(err) {
|
|
3048
|
+
console.error('Failed to remove API key:', err);
|
|
3049
|
+
});
|
|
3050
|
+
}
|
|
3051
|
+
|
|
3052
|
+
/**
|
|
3053
|
+
* Update API key field
|
|
3054
|
+
*/
|
|
3055
|
+
function updateApiKeyField(providerId, keyId, field, value) {
|
|
3056
|
+
fetch('/api/litellm-api/providers/' + providerId)
|
|
3057
|
+
.then(function(res) { return res.json(); })
|
|
3058
|
+
.then(function(provider) {
|
|
3059
|
+
const apiKeys = provider.apiKeys || [];
|
|
3060
|
+
const keyIndex = apiKeys.findIndex(function(k) { return k.id === keyId; });
|
|
3061
|
+
if (keyIndex >= 0) {
|
|
3062
|
+
apiKeys[keyIndex][field] = value;
|
|
3063
|
+
}
|
|
3064
|
+
return fetch('/api/litellm-api/providers/' + providerId, {
|
|
3065
|
+
method: 'PUT',
|
|
3066
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3067
|
+
body: JSON.stringify({ apiKeys: apiKeys })
|
|
3068
|
+
});
|
|
3069
|
+
})
|
|
3070
|
+
.catch(function(err) {
|
|
3071
|
+
console.error('Failed to update API key:', err);
|
|
3072
|
+
});
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
/**
|
|
3076
|
+
* Update provider routing strategy
|
|
3077
|
+
*/
|
|
3078
|
+
function updateProviderRouting(providerId, strategy) {
|
|
3079
|
+
fetch('/api/litellm-api/providers/' + providerId, {
|
|
3080
|
+
method: 'PUT',
|
|
3081
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3082
|
+
body: JSON.stringify({ routingStrategy: strategy })
|
|
3083
|
+
}).catch(function(err) {
|
|
3084
|
+
console.error('Failed to update routing:', err);
|
|
3085
|
+
});
|
|
3086
|
+
}
|
|
3087
|
+
|
|
3088
|
+
/**
|
|
3089
|
+
* Update health check enabled status
|
|
3090
|
+
*/
|
|
3091
|
+
function updateHealthCheckEnabled(providerId, enabled) {
|
|
3092
|
+
fetch('/api/litellm-api/providers/' + providerId)
|
|
3093
|
+
.then(function(res) { return res.json(); })
|
|
3094
|
+
.then(function(provider) {
|
|
3095
|
+
const healthCheck = provider.healthCheck || { intervalSeconds: 300, cooldownSeconds: 5, failureThreshold: 3 };
|
|
3096
|
+
healthCheck.enabled = enabled;
|
|
3097
|
+
return fetch('/api/litellm-api/providers/' + providerId, {
|
|
3098
|
+
method: 'PUT',
|
|
3099
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3100
|
+
body: JSON.stringify({ healthCheck: healthCheck })
|
|
3101
|
+
});
|
|
3102
|
+
})
|
|
3103
|
+
.then(function() {
|
|
3104
|
+
loadApiSettings().then(function() {
|
|
3105
|
+
refreshMultiKeyModal(providerId);
|
|
3106
|
+
});
|
|
3107
|
+
})
|
|
3108
|
+
.catch(function(err) {
|
|
3109
|
+
console.error('Failed to update health check:', err);
|
|
3110
|
+
});
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
/**
|
|
3114
|
+
* Update health check field
|
|
3115
|
+
*/
|
|
3116
|
+
function updateHealthCheckField(providerId, field, value) {
|
|
3117
|
+
fetch('/api/litellm-api/providers/' + providerId)
|
|
3118
|
+
.then(function(res) { return res.json(); })
|
|
3119
|
+
.then(function(provider) {
|
|
3120
|
+
const healthCheck = provider.healthCheck || { enabled: false, intervalSeconds: 300, cooldownSeconds: 5, failureThreshold: 3 };
|
|
3121
|
+
healthCheck[field] = value;
|
|
3122
|
+
return fetch('/api/litellm-api/providers/' + providerId, {
|
|
3123
|
+
method: 'PUT',
|
|
3124
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3125
|
+
body: JSON.stringify({ healthCheck: healthCheck })
|
|
3126
|
+
});
|
|
3127
|
+
})
|
|
3128
|
+
.catch(function(err) {
|
|
3129
|
+
console.error('Failed to update health check:', err);
|
|
3130
|
+
});
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
/**
|
|
3134
|
+
* Test API key
|
|
3135
|
+
*/
|
|
3136
|
+
function testApiKey(providerId, keyId) {
|
|
3137
|
+
const btn = event.target;
|
|
3138
|
+
btn.disabled = true;
|
|
3139
|
+
btn.classList.add('testing');
|
|
3140
|
+
btn.textContent = t('apiSettings.testingKey');
|
|
3141
|
+
|
|
3142
|
+
fetch('/api/litellm-api/providers/' + providerId + '/test-key', {
|
|
3143
|
+
method: 'POST',
|
|
3144
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3145
|
+
body: JSON.stringify({ keyId: keyId })
|
|
3146
|
+
})
|
|
3147
|
+
.then(function(res) { return res.json(); })
|
|
3148
|
+
.then(function(result) {
|
|
3149
|
+
btn.disabled = false;
|
|
3150
|
+
btn.classList.remove('testing');
|
|
3151
|
+
btn.textContent = t('apiSettings.testKey');
|
|
3152
|
+
|
|
3153
|
+
const keyItem = btn.closest('.api-key-item');
|
|
3154
|
+
const statusIndicator = keyItem.querySelector('.key-status-indicator');
|
|
3155
|
+
const statusText = keyItem.querySelector('.key-status-text');
|
|
3156
|
+
|
|
3157
|
+
if (result.valid) {
|
|
3158
|
+
statusIndicator.className = 'key-status-indicator healthy';
|
|
3159
|
+
statusText.textContent = t('apiSettings.healthy');
|
|
3160
|
+
showToast(t('apiSettings.keyValid'), 'success');
|
|
3161
|
+
} else {
|
|
3162
|
+
statusIndicator.className = 'key-status-indicator unhealthy';
|
|
3163
|
+
statusText.textContent = t('apiSettings.unhealthy');
|
|
3164
|
+
showToast(t('apiSettings.keyInvalid') + ': ' + (result.error || ''), 'error');
|
|
3165
|
+
}
|
|
3166
|
+
})
|
|
3167
|
+
.catch(function(err) {
|
|
3168
|
+
btn.disabled = false;
|
|
3169
|
+
btn.classList.remove('testing');
|
|
3170
|
+
btn.textContent = t('apiSettings.testKey');
|
|
3171
|
+
showToast('Test failed: ' + err.message, 'error');
|
|
3172
|
+
});
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
/**
|
|
3176
|
+
* Toggle key visibility
|
|
3177
|
+
*/
|
|
3178
|
+
function toggleKeyVisibility(btn) {
|
|
3179
|
+
const input = btn.previousElementSibling;
|
|
3180
|
+
if (input.type === 'password') {
|
|
3181
|
+
input.type = 'text';
|
|
3182
|
+
btn.textContent = '🔒';
|
|
3183
|
+
} else {
|
|
3184
|
+
input.type = 'password';
|
|
3185
|
+
btn.textContent = '👁️';
|
|
3186
|
+
}
|
|
3187
|
+
}
|
|
3188
|
+
|
|
3189
|
+
|
|
3190
|
+
// ========== CCW-LiteLLM Management ==========
|
|
3191
|
+
|
|
3192
|
+
/**
|
|
3193
|
+
* Check ccw-litellm installation status
|
|
3194
|
+
* @param {boolean} forceRefresh - Force refresh from server, bypass cache
|
|
3195
|
+
*/
|
|
3196
|
+
async function checkCcwLitellmStatus(forceRefresh = false) {
|
|
3197
|
+
// Check if cache is valid and not forcing refresh
|
|
3198
|
+
if (!forceRefresh && ccwLitellmStatusCache &&
|
|
3199
|
+
(Date.now() - ccwLitellmStatusCacheTime < CCW_LITELLM_STATUS_CACHE_TTL)) {
|
|
3200
|
+
console.log('[API Settings] Using cached ccw-litellm status');
|
|
3201
|
+
window.ccwLitellmStatus = ccwLitellmStatusCache;
|
|
3202
|
+
return ccwLitellmStatusCache;
|
|
3203
|
+
}
|
|
3204
|
+
|
|
3205
|
+
try {
|
|
3206
|
+
console.log('[API Settings] Checking ccw-litellm status from server...');
|
|
3207
|
+
// Add refresh=true to bypass backend cache when forceRefresh is true
|
|
3208
|
+
var statusUrl = '/api/litellm-api/ccw-litellm/status' + (forceRefresh ? '?refresh=true' : '');
|
|
3209
|
+
var response = await fetch(statusUrl);
|
|
3210
|
+
console.log('[API Settings] Status response:', response.status);
|
|
3211
|
+
var status = await response.json();
|
|
3212
|
+
console.log('[API Settings] ccw-litellm status:', status);
|
|
3213
|
+
|
|
3214
|
+
// Update cache
|
|
3215
|
+
ccwLitellmStatusCache = status;
|
|
3216
|
+
ccwLitellmStatusCacheTime = Date.now();
|
|
3217
|
+
window.ccwLitellmStatus = status;
|
|
3218
|
+
|
|
3219
|
+
return status;
|
|
3220
|
+
} catch (e) {
|
|
3221
|
+
console.warn('[API Settings] Could not check ccw-litellm status:', e);
|
|
3222
|
+
var fallbackStatus = { installed: false };
|
|
3223
|
+
|
|
3224
|
+
// Cache the fallback result too
|
|
3225
|
+
ccwLitellmStatusCache = fallbackStatus;
|
|
3226
|
+
ccwLitellmStatusCacheTime = Date.now();
|
|
3227
|
+
|
|
3228
|
+
return fallbackStatus;
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
/**
|
|
3233
|
+
* Render ccw-litellm status card
|
|
3234
|
+
*/
|
|
3235
|
+
function renderCcwLitellmStatusCard() {
|
|
3236
|
+
var container = document.getElementById('ccwLitellmStatusContainer');
|
|
3237
|
+
if (!container) return;
|
|
3238
|
+
|
|
3239
|
+
var status = window.ccwLitellmStatus || { installed: false };
|
|
3240
|
+
|
|
3241
|
+
if (status.installed) {
|
|
3242
|
+
container.innerHTML =
|
|
3243
|
+
'<div class="flex items-center gap-2 text-sm">' +
|
|
3244
|
+
'<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-success/10 text-success border border-success/20">' +
|
|
3245
|
+
'<i data-lucide="check-circle" class="w-3.5 h-3.5"></i>' +
|
|
3246
|
+
'ccw-litellm ' + (status.version || '') +
|
|
3247
|
+
'</span>' +
|
|
3248
|
+
'<button class="btn-sm btn-outline-danger" onclick="uninstallCcwLitellm()" title="Uninstall ccw-litellm">' +
|
|
3249
|
+
'<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>' +
|
|
3250
|
+
'</button>' +
|
|
3251
|
+
'</div>';
|
|
3252
|
+
} else {
|
|
3253
|
+
container.innerHTML =
|
|
3254
|
+
'<div class="flex items-center gap-2">' +
|
|
3255
|
+
'<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-muted text-muted-foreground border border-border text-sm">' +
|
|
3256
|
+
'<i data-lucide="circle" class="w-3.5 h-3.5"></i>' +
|
|
3257
|
+
'ccw-litellm not installed' +
|
|
3258
|
+
'</span>' +
|
|
3259
|
+
'<button class="btn-sm btn-primary" onclick="installCcwLitellm()">' +
|
|
3260
|
+
'<i data-lucide="download" class="w-3.5 h-3.5"></i> Install' +
|
|
3261
|
+
'</button>' +
|
|
3262
|
+
'</div>';
|
|
3263
|
+
}
|
|
3264
|
+
|
|
3265
|
+
if (window.lucide) lucide.createIcons();
|
|
3266
|
+
}
|
|
3267
|
+
|
|
3268
|
+
/**
|
|
3269
|
+
* Install ccw-litellm package
|
|
3270
|
+
*/
|
|
3271
|
+
async function installCcwLitellm() {
|
|
3272
|
+
var container = document.getElementById('ccwLitellmStatusContainer');
|
|
3273
|
+
if (container) {
|
|
3274
|
+
container.innerHTML =
|
|
3275
|
+
'<div class="flex items-center gap-2 text-sm text-muted-foreground">' +
|
|
3276
|
+
'<div class="animate-spin w-4 h-4 border-2 border-primary border-t-transparent rounded-full"></div>' +
|
|
3277
|
+
'Installing ccw-litellm...' +
|
|
3278
|
+
'</div>';
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3281
|
+
try {
|
|
3282
|
+
var response = await fetch('/api/litellm-api/ccw-litellm/install', {
|
|
3283
|
+
method: 'POST',
|
|
3284
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3285
|
+
body: JSON.stringify({})
|
|
3286
|
+
});
|
|
3287
|
+
|
|
3288
|
+
var result = await response.json();
|
|
3289
|
+
|
|
3290
|
+
if (result.success) {
|
|
3291
|
+
showRefreshToast('ccw-litellm installed successfully!', 'success');
|
|
3292
|
+
// Refresh status (force refresh after installation)
|
|
3293
|
+
await checkCcwLitellmStatus(true);
|
|
3294
|
+
renderCcwLitellmStatusCard();
|
|
3295
|
+
} else {
|
|
3296
|
+
showRefreshToast('Failed to install ccw-litellm: ' + result.error, 'error');
|
|
3297
|
+
renderCcwLitellmStatusCard();
|
|
3298
|
+
}
|
|
3299
|
+
} catch (e) {
|
|
3300
|
+
showRefreshToast('Installation error: ' + e.message, 'error');
|
|
3301
|
+
renderCcwLitellmStatusCard();
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
|
|
3305
|
+
/**
|
|
3306
|
+
* Uninstall ccw-litellm package
|
|
3307
|
+
*/
|
|
3308
|
+
async function uninstallCcwLitellm() {
|
|
3309
|
+
if (!confirm('Are you sure you want to uninstall ccw-litellm? This will disable LiteLLM features.')) {
|
|
3310
|
+
return;
|
|
3311
|
+
}
|
|
3312
|
+
|
|
3313
|
+
var container = document.getElementById('ccwLitellmStatusContainer');
|
|
3314
|
+
if (container) {
|
|
3315
|
+
container.innerHTML =
|
|
3316
|
+
'<div class="flex items-center gap-2 text-sm text-muted-foreground">' +
|
|
3317
|
+
'<div class="animate-spin w-4 h-4 border-2 border-primary border-t-transparent rounded-full"></div>' +
|
|
3318
|
+
'Uninstalling ccw-litellm...' +
|
|
3319
|
+
'</div>';
|
|
3320
|
+
}
|
|
3321
|
+
|
|
3322
|
+
try {
|
|
3323
|
+
var response = await fetch('/api/litellm-api/ccw-litellm/uninstall', {
|
|
3324
|
+
method: 'POST',
|
|
3325
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3326
|
+
body: JSON.stringify({})
|
|
3327
|
+
});
|
|
3328
|
+
|
|
3329
|
+
var result = await response.json();
|
|
3330
|
+
|
|
3331
|
+
if (result.success) {
|
|
3332
|
+
showRefreshToast('ccw-litellm uninstalled successfully!', 'success');
|
|
3333
|
+
await checkCcwLitellmStatus(true);
|
|
3334
|
+
renderCcwLitellmStatusCard();
|
|
3335
|
+
} else {
|
|
3336
|
+
showRefreshToast('Failed to uninstall ccw-litellm: ' + result.error, 'error');
|
|
3337
|
+
renderCcwLitellmStatusCard();
|
|
3338
|
+
}
|
|
3339
|
+
} catch (e) {
|
|
3340
|
+
showRefreshToast('Uninstall error: ' + e.message, 'error');
|
|
3341
|
+
renderCcwLitellmStatusCard();
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
|
|
3345
|
+
// Make functions globally accessible
|
|
3346
|
+
window.checkCcwLitellmStatus = checkCcwLitellmStatus;
|
|
3347
|
+
window.renderCcwLitellmStatusCard = renderCcwLitellmStatusCard;
|
|
3348
|
+
window.installCcwLitellm = installCcwLitellm;
|
|
3349
|
+
window.uninstallCcwLitellm = uninstallCcwLitellm;
|
|
3350
|
+
|
|
3351
|
+
|
|
3352
|
+
// ========== Utility Functions ==========
|
|
3353
|
+
|
|
3354
|
+
/**
|
|
3355
|
+
* Mask API key for display
|
|
3356
|
+
*/
|
|
3357
|
+
function maskApiKey(apiKey) {
|
|
3358
|
+
if (!apiKey) return '';
|
|
3359
|
+
if (apiKey.startsWith('${')) return apiKey; // Environment variable
|
|
3360
|
+
if (apiKey.length <= 8) return '***';
|
|
3361
|
+
return apiKey.substring(0, 4) + '...' + apiKey.substring(apiKey.length - 4);
|
|
3362
|
+
}
|