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.
Files changed (208) hide show
  1. package/.claude/CLAUDE.md +16 -1
  2. package/.claude/workflows/cli-templates/protocols/analysis-protocol.md +11 -4
  3. package/.claude/workflows/cli-templates/protocols/write-protocol.md +10 -75
  4. package/.claude/workflows/cli-tools-usage.md +14 -24
  5. package/.codex/AGENTS.md +51 -1
  6. package/.codex/prompts/compact.md +378 -0
  7. package/.gemini/GEMINI.md +57 -20
  8. package/ccw/dist/cli.d.ts.map +1 -1
  9. package/ccw/dist/cli.js +21 -8
  10. package/ccw/dist/cli.js.map +1 -1
  11. package/ccw/dist/commands/cli.d.ts +2 -0
  12. package/ccw/dist/commands/cli.d.ts.map +1 -1
  13. package/ccw/dist/commands/cli.js +129 -8
  14. package/ccw/dist/commands/cli.js.map +1 -1
  15. package/ccw/dist/commands/hook.d.ts.map +1 -1
  16. package/ccw/dist/commands/hook.js +3 -2
  17. package/ccw/dist/commands/hook.js.map +1 -1
  18. package/ccw/dist/config/litellm-api-config-manager.d.ts +180 -0
  19. package/ccw/dist/config/litellm-api-config-manager.d.ts.map +1 -0
  20. package/ccw/dist/config/litellm-api-config-manager.js +770 -0
  21. package/ccw/dist/config/litellm-api-config-manager.js.map +1 -0
  22. package/ccw/dist/config/provider-models.d.ts +73 -0
  23. package/ccw/dist/config/provider-models.d.ts.map +1 -0
  24. package/ccw/dist/config/provider-models.js +172 -0
  25. package/ccw/dist/config/provider-models.js.map +1 -0
  26. package/ccw/dist/core/cache-manager.d.ts.map +1 -1
  27. package/ccw/dist/core/cache-manager.js +3 -5
  28. package/ccw/dist/core/cache-manager.js.map +1 -1
  29. package/ccw/dist/core/dashboard-generator.d.ts.map +1 -1
  30. package/ccw/dist/core/dashboard-generator.js +3 -1
  31. package/ccw/dist/core/dashboard-generator.js.map +1 -1
  32. package/ccw/dist/core/routes/cli-routes.d.ts.map +1 -1
  33. package/ccw/dist/core/routes/cli-routes.js +169 -0
  34. package/ccw/dist/core/routes/cli-routes.js.map +1 -1
  35. package/ccw/dist/core/routes/codexlens-routes.d.ts.map +1 -1
  36. package/ccw/dist/core/routes/codexlens-routes.js +234 -18
  37. package/ccw/dist/core/routes/codexlens-routes.js.map +1 -1
  38. package/ccw/dist/core/routes/hooks-routes.d.ts.map +1 -1
  39. package/ccw/dist/core/routes/hooks-routes.js +30 -32
  40. package/ccw/dist/core/routes/hooks-routes.js.map +1 -1
  41. package/ccw/dist/core/routes/litellm-api-routes.d.ts +21 -0
  42. package/ccw/dist/core/routes/litellm-api-routes.d.ts.map +1 -0
  43. package/ccw/dist/core/routes/litellm-api-routes.js +780 -0
  44. package/ccw/dist/core/routes/litellm-api-routes.js.map +1 -0
  45. package/ccw/dist/core/routes/litellm-routes.d.ts +20 -0
  46. package/ccw/dist/core/routes/litellm-routes.d.ts.map +1 -0
  47. package/ccw/dist/core/routes/litellm-routes.js +85 -0
  48. package/ccw/dist/core/routes/litellm-routes.js.map +1 -0
  49. package/ccw/dist/core/routes/mcp-routes.js +2 -2
  50. package/ccw/dist/core/routes/mcp-routes.js.map +1 -1
  51. package/ccw/dist/core/routes/status-routes.d.ts.map +1 -1
  52. package/ccw/dist/core/routes/status-routes.js +39 -0
  53. package/ccw/dist/core/routes/status-routes.js.map +1 -1
  54. package/ccw/dist/core/routes/system-routes.js +1 -1
  55. package/ccw/dist/core/routes/system-routes.js.map +1 -1
  56. package/ccw/dist/core/server.d.ts.map +1 -1
  57. package/ccw/dist/core/server.js +15 -1
  58. package/ccw/dist/core/server.js.map +1 -1
  59. package/ccw/dist/mcp-server/index.js +1 -1
  60. package/ccw/dist/mcp-server/index.js.map +1 -1
  61. package/ccw/dist/tools/claude-cli-tools.d.ts +82 -0
  62. package/ccw/dist/tools/claude-cli-tools.d.ts.map +1 -0
  63. package/ccw/dist/tools/claude-cli-tools.js +216 -0
  64. package/ccw/dist/tools/claude-cli-tools.js.map +1 -0
  65. package/ccw/dist/tools/cli-executor.d.ts.map +1 -1
  66. package/ccw/dist/tools/cli-executor.js +76 -14
  67. package/ccw/dist/tools/cli-executor.js.map +1 -1
  68. package/ccw/dist/tools/codex-lens.d.ts +9 -2
  69. package/ccw/dist/tools/codex-lens.d.ts.map +1 -1
  70. package/ccw/dist/tools/codex-lens.js +114 -9
  71. package/ccw/dist/tools/codex-lens.js.map +1 -1
  72. package/ccw/dist/tools/context-cache-store.d.ts +136 -0
  73. package/ccw/dist/tools/context-cache-store.d.ts.map +1 -0
  74. package/ccw/dist/tools/context-cache-store.js +256 -0
  75. package/ccw/dist/tools/context-cache-store.js.map +1 -0
  76. package/ccw/dist/tools/context-cache.d.ts +56 -0
  77. package/ccw/dist/tools/context-cache.d.ts.map +1 -0
  78. package/ccw/dist/tools/context-cache.js +294 -0
  79. package/ccw/dist/tools/context-cache.js.map +1 -0
  80. package/ccw/dist/tools/core-memory.d.ts.map +1 -1
  81. package/ccw/dist/tools/core-memory.js +33 -19
  82. package/ccw/dist/tools/core-memory.js.map +1 -1
  83. package/ccw/dist/tools/index.d.ts.map +1 -1
  84. package/ccw/dist/tools/index.js +2 -0
  85. package/ccw/dist/tools/index.js.map +1 -1
  86. package/ccw/dist/tools/litellm-client.d.ts +85 -0
  87. package/ccw/dist/tools/litellm-client.d.ts.map +1 -0
  88. package/ccw/dist/tools/litellm-client.js +188 -0
  89. package/ccw/dist/tools/litellm-client.js.map +1 -0
  90. package/ccw/dist/tools/litellm-executor.d.ts +34 -0
  91. package/ccw/dist/tools/litellm-executor.d.ts.map +1 -0
  92. package/ccw/dist/tools/litellm-executor.js +192 -0
  93. package/ccw/dist/tools/litellm-executor.js.map +1 -0
  94. package/ccw/dist/tools/pattern-parser.d.ts +55 -0
  95. package/ccw/dist/tools/pattern-parser.d.ts.map +1 -0
  96. package/ccw/dist/tools/pattern-parser.js +237 -0
  97. package/ccw/dist/tools/pattern-parser.js.map +1 -0
  98. package/ccw/dist/tools/smart-search.d.ts +1 -0
  99. package/ccw/dist/tools/smart-search.d.ts.map +1 -1
  100. package/ccw/dist/tools/smart-search.js +117 -41
  101. package/ccw/dist/tools/smart-search.js.map +1 -1
  102. package/ccw/dist/types/litellm-api-config.d.ts +294 -0
  103. package/ccw/dist/types/litellm-api-config.d.ts.map +1 -0
  104. package/ccw/dist/types/litellm-api-config.js +8 -0
  105. package/ccw/dist/types/litellm-api-config.js.map +1 -0
  106. package/ccw/src/cli.ts +258 -244
  107. package/ccw/src/commands/cli.ts +153 -9
  108. package/ccw/src/commands/hook.ts +3 -2
  109. package/ccw/src/config/.litellm-api-config-manager.ts.2025-12-23T11-57-43-727Z.bak +441 -0
  110. package/ccw/src/config/litellm-api-config-manager.ts +1012 -0
  111. package/ccw/src/config/provider-models.ts +222 -0
  112. package/ccw/src/core/cache-manager.ts +292 -294
  113. package/ccw/src/core/dashboard-generator.ts +3 -1
  114. package/ccw/src/core/routes/cli-routes.ts +192 -0
  115. package/ccw/src/core/routes/codexlens-routes.ts +241 -19
  116. package/ccw/src/core/routes/hooks-routes.ts +399 -405
  117. package/ccw/src/core/routes/litellm-api-routes.ts +930 -0
  118. package/ccw/src/core/routes/litellm-routes.ts +107 -0
  119. package/ccw/src/core/routes/mcp-routes.ts +1271 -1271
  120. package/ccw/src/core/routes/status-routes.ts +51 -0
  121. package/ccw/src/core/routes/system-routes.ts +1 -1
  122. package/ccw/src/core/server.ts +15 -1
  123. package/ccw/src/mcp-server/index.ts +1 -1
  124. package/ccw/src/templates/dashboard-css/12-cli-legacy.css +44 -0
  125. package/ccw/src/templates/dashboard-css/31-api-settings.css +2265 -0
  126. package/ccw/src/templates/dashboard-js/components/cli-history.js +15 -8
  127. package/ccw/src/templates/dashboard-js/components/cli-status.js +323 -9
  128. package/ccw/src/templates/dashboard-js/components/navigation.js +329 -313
  129. package/ccw/src/templates/dashboard-js/i18n.js +583 -1
  130. package/ccw/src/templates/dashboard-js/views/api-settings.js +3362 -0
  131. package/ccw/src/templates/dashboard-js/views/cli-manager.js +199 -24
  132. package/ccw/src/templates/dashboard-js/views/codexlens-manager.js +1265 -27
  133. package/ccw/src/templates/dashboard.html +840 -831
  134. package/ccw/src/tools/claude-cli-tools.ts +300 -0
  135. package/ccw/src/tools/cli-executor.ts +83 -14
  136. package/ccw/src/tools/codex-lens.ts +146 -9
  137. package/ccw/src/tools/context-cache-store.ts +368 -0
  138. package/ccw/src/tools/context-cache.ts +393 -0
  139. package/ccw/src/tools/core-memory.ts +33 -19
  140. package/ccw/src/tools/index.ts +2 -0
  141. package/ccw/src/tools/litellm-client.ts +246 -0
  142. package/ccw/src/tools/litellm-executor.ts +241 -0
  143. package/ccw/src/tools/pattern-parser.ts +329 -0
  144. package/ccw/src/tools/smart-search.ts +142 -41
  145. package/ccw/src/types/litellm-api-config.ts +402 -0
  146. package/ccw-litellm/README.md +180 -0
  147. package/ccw-litellm/pyproject.toml +35 -0
  148. package/ccw-litellm/src/ccw_litellm/__init__.py +47 -0
  149. package/ccw-litellm/src/ccw_litellm/__pycache__/__init__.cpython-313.pyc +0 -0
  150. package/ccw-litellm/src/ccw_litellm/__pycache__/cli.cpython-313.pyc +0 -0
  151. package/ccw-litellm/src/ccw_litellm/cli.py +108 -0
  152. package/ccw-litellm/src/ccw_litellm/clients/__init__.py +12 -0
  153. package/ccw-litellm/src/ccw_litellm/clients/__pycache__/__init__.cpython-313.pyc +0 -0
  154. package/ccw-litellm/src/ccw_litellm/clients/__pycache__/litellm_embedder.cpython-313.pyc +0 -0
  155. package/ccw-litellm/src/ccw_litellm/clients/__pycache__/litellm_llm.cpython-313.pyc +0 -0
  156. package/ccw-litellm/src/ccw_litellm/clients/litellm_embedder.py +251 -0
  157. package/ccw-litellm/src/ccw_litellm/clients/litellm_llm.py +165 -0
  158. package/ccw-litellm/src/ccw_litellm/config/__init__.py +22 -0
  159. package/ccw-litellm/src/ccw_litellm/config/__pycache__/__init__.cpython-313.pyc +0 -0
  160. package/ccw-litellm/src/ccw_litellm/config/__pycache__/loader.cpython-313.pyc +0 -0
  161. package/ccw-litellm/src/ccw_litellm/config/__pycache__/models.cpython-313.pyc +0 -0
  162. package/ccw-litellm/src/ccw_litellm/config/loader.py +316 -0
  163. package/ccw-litellm/src/ccw_litellm/config/models.py +130 -0
  164. package/ccw-litellm/src/ccw_litellm/interfaces/__init__.py +14 -0
  165. package/ccw-litellm/src/ccw_litellm/interfaces/__pycache__/__init__.cpython-313.pyc +0 -0
  166. package/ccw-litellm/src/ccw_litellm/interfaces/__pycache__/embedder.cpython-313.pyc +0 -0
  167. package/ccw-litellm/src/ccw_litellm/interfaces/__pycache__/llm.cpython-313.pyc +0 -0
  168. package/ccw-litellm/src/ccw_litellm/interfaces/embedder.py +52 -0
  169. package/ccw-litellm/src/ccw_litellm/interfaces/llm.py +45 -0
  170. package/codex-lens/src/codexlens/__pycache__/config.cpython-313.pyc +0 -0
  171. package/codex-lens/src/codexlens/cli/__pycache__/commands.cpython-313.pyc +0 -0
  172. package/codex-lens/src/codexlens/cli/__pycache__/embedding_manager.cpython-313.pyc +0 -0
  173. package/codex-lens/src/codexlens/cli/__pycache__/model_manager.cpython-313.pyc +0 -0
  174. package/codex-lens/src/codexlens/cli/__pycache__/output.cpython-313.pyc +0 -0
  175. package/codex-lens/src/codexlens/cli/commands.py +378 -23
  176. package/codex-lens/src/codexlens/cli/embedding_manager.py +660 -56
  177. package/codex-lens/src/codexlens/cli/model_manager.py +31 -18
  178. package/codex-lens/src/codexlens/cli/output.py +12 -1
  179. package/codex-lens/src/codexlens/config.py +93 -0
  180. package/codex-lens/src/codexlens/search/__pycache__/chain_search.cpython-313.pyc +0 -0
  181. package/codex-lens/src/codexlens/search/__pycache__/hybrid_search.cpython-313.pyc +0 -0
  182. package/codex-lens/src/codexlens/search/__pycache__/ranking.cpython-313.pyc +0 -0
  183. package/codex-lens/src/codexlens/search/chain_search.py +6 -2
  184. package/codex-lens/src/codexlens/search/hybrid_search.py +44 -21
  185. package/codex-lens/src/codexlens/search/ranking.py +1 -1
  186. package/codex-lens/src/codexlens/semantic/__init__.py +42 -0
  187. package/codex-lens/src/codexlens/semantic/__pycache__/__init__.cpython-313.pyc +0 -0
  188. package/codex-lens/src/codexlens/semantic/__pycache__/base.cpython-313.pyc +0 -0
  189. package/codex-lens/src/codexlens/semantic/__pycache__/chunker.cpython-313.pyc +0 -0
  190. package/codex-lens/src/codexlens/semantic/__pycache__/embedder.cpython-313.pyc +0 -0
  191. package/codex-lens/src/codexlens/semantic/__pycache__/factory.cpython-313.pyc +0 -0
  192. package/codex-lens/src/codexlens/semantic/__pycache__/gpu_support.cpython-313.pyc +0 -0
  193. package/codex-lens/src/codexlens/semantic/__pycache__/litellm_embedder.cpython-313.pyc +0 -0
  194. package/codex-lens/src/codexlens/semantic/__pycache__/vector_store.cpython-313.pyc +0 -0
  195. package/codex-lens/src/codexlens/semantic/base.py +61 -0
  196. package/codex-lens/src/codexlens/semantic/chunker.py +43 -20
  197. package/codex-lens/src/codexlens/semantic/embedder.py +60 -13
  198. package/codex-lens/src/codexlens/semantic/factory.py +98 -0
  199. package/codex-lens/src/codexlens/semantic/gpu_support.py +225 -3
  200. package/codex-lens/src/codexlens/semantic/litellm_embedder.py +144 -0
  201. package/codex-lens/src/codexlens/semantic/rotational_embedder.py +434 -0
  202. package/codex-lens/src/codexlens/semantic/vector_store.py +33 -8
  203. package/codex-lens/src/codexlens/storage/__pycache__/path_mapper.cpython-313.pyc +0 -0
  204. package/codex-lens/src/codexlens/storage/migrations/__pycache__/migration_004_dual_fts.cpython-313.pyc +0 -0
  205. package/codex-lens/src/codexlens/storage/path_mapper.py +27 -1
  206. package/package.json +15 -5
  207. package/.codex/prompts.zip +0 -0
  208. 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()">&times;</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()">&times;</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, '&amp;')
1518
+ .replace(/</g, '&lt;')
1519
+ .replace(/>/g, '&gt;')
1520
+ .replace(/"/g, '&quot;')
1521
+ .replace(/'/g, '&#039;');
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()">&times;</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()">&times;</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()">&times;</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
+ }