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,1012 @@
1
+ /**
2
+ * LiteLLM API Configuration Manager
3
+ * Manages provider credentials, custom endpoints, and cache settings
4
+ */
5
+
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
7
+ import { homedir } from 'os';
8
+ import { join } from 'path';
9
+ import { StoragePaths, GlobalPaths, ensureStorageDir } from './storage-paths.js';
10
+ import type {
11
+ LiteLLMApiConfig,
12
+ ProviderCredential,
13
+ CustomEndpoint,
14
+ GlobalCacheSettings,
15
+ ProviderType,
16
+ CacheStrategy,
17
+ CodexLensEmbeddingRotation,
18
+ CodexLensEmbeddingProvider,
19
+ EmbeddingPoolConfig,
20
+ } from '../types/litellm-api-config.js';
21
+
22
+ /**
23
+ * Default configuration
24
+ */
25
+ function getDefaultConfig(): LiteLLMApiConfig {
26
+ return {
27
+ version: 1,
28
+ providers: [],
29
+ endpoints: [],
30
+ globalCacheSettings: {
31
+ enabled: true,
32
+ cacheDir: '~/.ccw/cache/context',
33
+ maxTotalSizeMB: 100,
34
+ },
35
+ };
36
+ }
37
+
38
+ /**
39
+ * Get config file path (global, shared across all projects)
40
+ */
41
+ function getConfigPath(_baseDir?: string): string {
42
+ const configDir = GlobalPaths.config();
43
+ ensureStorageDir(configDir);
44
+ return join(configDir, 'litellm-api-config.json');
45
+ }
46
+
47
+ /**
48
+ * Load configuration from file
49
+ */
50
+ export function loadLiteLLMApiConfig(baseDir: string): LiteLLMApiConfig {
51
+ const configPath = getConfigPath(baseDir);
52
+
53
+ if (!existsSync(configPath)) {
54
+ return getDefaultConfig();
55
+ }
56
+
57
+ try {
58
+ const content = readFileSync(configPath, 'utf-8');
59
+ return JSON.parse(content) as LiteLLMApiConfig;
60
+ } catch (error) {
61
+ console.error('[LiteLLM Config] Failed to load config:', error);
62
+ return getDefaultConfig();
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Save configuration to file
68
+ */
69
+ function saveConfig(baseDir: string, config: LiteLLMApiConfig): void {
70
+ const configPath = getConfigPath(baseDir);
71
+ writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
72
+ }
73
+
74
+ /**
75
+ * Resolve environment variables in API key
76
+ * Supports ${ENV_VAR} syntax
77
+ */
78
+ export function resolveEnvVar(value: string): string {
79
+ if (!value) return value;
80
+
81
+ const envVarMatch = value.match(/^\$\{(.+)\}$/);
82
+ if (envVarMatch) {
83
+ const envVarName = envVarMatch[1];
84
+ return process.env[envVarName] || '';
85
+ }
86
+
87
+ return value;
88
+ }
89
+
90
+ // ===========================
91
+ // Provider Management
92
+ // ===========================
93
+
94
+ /**
95
+ * Get all providers
96
+ */
97
+ export function getAllProviders(baseDir: string): ProviderCredential[] {
98
+ const config = loadLiteLLMApiConfig(baseDir);
99
+ return config.providers;
100
+ }
101
+
102
+ /**
103
+ * Get provider by ID
104
+ */
105
+ export function getProvider(baseDir: string, providerId: string): ProviderCredential | null {
106
+ const config = loadLiteLLMApiConfig(baseDir);
107
+ return config.providers.find((p) => p.id === providerId) || null;
108
+ }
109
+
110
+ /**
111
+ * Get provider with resolved environment variables
112
+ */
113
+ export function getProviderWithResolvedEnvVars(
114
+ baseDir: string,
115
+ providerId: string
116
+ ): (ProviderCredential & { resolvedApiKey: string }) | null {
117
+ const provider = getProvider(baseDir, providerId);
118
+ if (!provider) return null;
119
+
120
+ return {
121
+ ...provider,
122
+ resolvedApiKey: resolveEnvVar(provider.apiKey),
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Add new provider
128
+ */
129
+ export function addProvider(
130
+ baseDir: string,
131
+ providerData: Omit<ProviderCredential, 'id' | 'createdAt' | 'updatedAt'>
132
+ ): ProviderCredential {
133
+ const config = loadLiteLLMApiConfig(baseDir);
134
+
135
+ const provider: ProviderCredential = {
136
+ ...providerData,
137
+ id: `${providerData.type}-${Date.now()}`,
138
+ createdAt: new Date().toISOString(),
139
+ updatedAt: new Date().toISOString(),
140
+ };
141
+
142
+ config.providers.push(provider);
143
+ saveConfig(baseDir, config);
144
+
145
+ return provider;
146
+ }
147
+
148
+ /**
149
+ * Update provider
150
+ */
151
+ export function updateProvider(
152
+ baseDir: string,
153
+ providerId: string,
154
+ updates: Partial<Omit<ProviderCredential, 'id' | 'createdAt' | 'updatedAt'>>
155
+ ): ProviderCredential {
156
+ const config = loadLiteLLMApiConfig(baseDir);
157
+ const providerIndex = config.providers.findIndex((p) => p.id === providerId);
158
+
159
+ if (providerIndex === -1) {
160
+ throw new Error(`Provider not found: ${providerId}`);
161
+ }
162
+
163
+ config.providers[providerIndex] = {
164
+ ...config.providers[providerIndex],
165
+ ...updates,
166
+ updatedAt: new Date().toISOString(),
167
+ };
168
+
169
+ saveConfig(baseDir, config);
170
+ return config.providers[providerIndex];
171
+ }
172
+
173
+ /**
174
+ * Delete provider
175
+ */
176
+ export function deleteProvider(baseDir: string, providerId: string): boolean {
177
+ const config = loadLiteLLMApiConfig(baseDir);
178
+ const initialLength = config.providers.length;
179
+
180
+ config.providers = config.providers.filter((p) => p.id !== providerId);
181
+
182
+ if (config.providers.length === initialLength) {
183
+ return false;
184
+ }
185
+
186
+ // Also remove endpoints using this provider
187
+ config.endpoints = config.endpoints.filter((e) => e.providerId !== providerId);
188
+
189
+ saveConfig(baseDir, config);
190
+ return true;
191
+ }
192
+
193
+ // ===========================
194
+ // Endpoint Management
195
+ // ===========================
196
+
197
+ /**
198
+ * Get all endpoints
199
+ */
200
+ export function getAllEndpoints(baseDir: string): CustomEndpoint[] {
201
+ const config = loadLiteLLMApiConfig(baseDir);
202
+ return config.endpoints;
203
+ }
204
+
205
+ /**
206
+ * Get endpoint by ID
207
+ */
208
+ export function getEndpoint(baseDir: string, endpointId: string): CustomEndpoint | null {
209
+ const config = loadLiteLLMApiConfig(baseDir);
210
+ return config.endpoints.find((e) => e.id === endpointId) || null;
211
+ }
212
+
213
+ /**
214
+ * Find endpoint by ID (alias for getEndpoint)
215
+ */
216
+ export function findEndpointById(baseDir: string, endpointId: string): CustomEndpoint | null {
217
+ return getEndpoint(baseDir, endpointId);
218
+ }
219
+
220
+ /**
221
+ * Add new endpoint
222
+ */
223
+ export function addEndpoint(
224
+ baseDir: string,
225
+ endpointData: Omit<CustomEndpoint, 'createdAt' | 'updatedAt'>
226
+ ): CustomEndpoint {
227
+ const config = loadLiteLLMApiConfig(baseDir);
228
+
229
+ // Check if ID already exists
230
+ if (config.endpoints.some((e) => e.id === endpointData.id)) {
231
+ throw new Error(`Endpoint ID already exists: ${endpointData.id}`);
232
+ }
233
+
234
+ // Verify provider exists
235
+ if (!config.providers.find((p) => p.id === endpointData.providerId)) {
236
+ throw new Error(`Provider not found: ${endpointData.providerId}`);
237
+ }
238
+
239
+ const endpoint: CustomEndpoint = {
240
+ ...endpointData,
241
+ createdAt: new Date().toISOString(),
242
+ updatedAt: new Date().toISOString(),
243
+ };
244
+
245
+ config.endpoints.push(endpoint);
246
+ saveConfig(baseDir, config);
247
+
248
+ return endpoint;
249
+ }
250
+
251
+ /**
252
+ * Update endpoint
253
+ */
254
+ export function updateEndpoint(
255
+ baseDir: string,
256
+ endpointId: string,
257
+ updates: Partial<Omit<CustomEndpoint, 'id' | 'createdAt' | 'updatedAt'>>
258
+ ): CustomEndpoint {
259
+ const config = loadLiteLLMApiConfig(baseDir);
260
+ const endpointIndex = config.endpoints.findIndex((e) => e.id === endpointId);
261
+
262
+ if (endpointIndex === -1) {
263
+ throw new Error(`Endpoint not found: ${endpointId}`);
264
+ }
265
+
266
+ // Verify provider exists if updating providerId
267
+ if (updates.providerId && !config.providers.find((p) => p.id === updates.providerId)) {
268
+ throw new Error(`Provider not found: ${updates.providerId}`);
269
+ }
270
+
271
+ config.endpoints[endpointIndex] = {
272
+ ...config.endpoints[endpointIndex],
273
+ ...updates,
274
+ updatedAt: new Date().toISOString(),
275
+ };
276
+
277
+ saveConfig(baseDir, config);
278
+ return config.endpoints[endpointIndex];
279
+ }
280
+
281
+ /**
282
+ * Delete endpoint
283
+ */
284
+ export function deleteEndpoint(baseDir: string, endpointId: string): boolean {
285
+ const config = loadLiteLLMApiConfig(baseDir);
286
+ const initialLength = config.endpoints.length;
287
+
288
+ config.endpoints = config.endpoints.filter((e) => e.id !== endpointId);
289
+
290
+ if (config.endpoints.length === initialLength) {
291
+ return false;
292
+ }
293
+
294
+ // Clear default endpoint if deleted
295
+ if (config.defaultEndpoint === endpointId) {
296
+ delete config.defaultEndpoint;
297
+ }
298
+
299
+ saveConfig(baseDir, config);
300
+ return true;
301
+ }
302
+
303
+ // ===========================
304
+ // Default Endpoint Management
305
+ // ===========================
306
+
307
+ /**
308
+ * Get default endpoint
309
+ */
310
+ export function getDefaultEndpoint(baseDir: string): string | undefined {
311
+ const config = loadLiteLLMApiConfig(baseDir);
312
+ return config.defaultEndpoint;
313
+ }
314
+
315
+ /**
316
+ * Set default endpoint
317
+ */
318
+ export function setDefaultEndpoint(baseDir: string, endpointId?: string): void {
319
+ const config = loadLiteLLMApiConfig(baseDir);
320
+
321
+ if (endpointId) {
322
+ // Verify endpoint exists
323
+ if (!config.endpoints.find((e) => e.id === endpointId)) {
324
+ throw new Error(`Endpoint not found: ${endpointId}`);
325
+ }
326
+ config.defaultEndpoint = endpointId;
327
+ } else {
328
+ delete config.defaultEndpoint;
329
+ }
330
+
331
+ saveConfig(baseDir, config);
332
+ }
333
+
334
+ // ===========================
335
+ // Cache Settings Management
336
+ // ===========================
337
+
338
+ /**
339
+ * Get global cache settings
340
+ */
341
+ export function getGlobalCacheSettings(baseDir: string): GlobalCacheSettings {
342
+ const config = loadLiteLLMApiConfig(baseDir);
343
+ return config.globalCacheSettings;
344
+ }
345
+
346
+ /**
347
+ * Update global cache settings
348
+ */
349
+ export function updateGlobalCacheSettings(
350
+ baseDir: string,
351
+ settings: Partial<GlobalCacheSettings>
352
+ ): void {
353
+ const config = loadLiteLLMApiConfig(baseDir);
354
+
355
+ config.globalCacheSettings = {
356
+ ...config.globalCacheSettings,
357
+ ...settings,
358
+ };
359
+
360
+ saveConfig(baseDir, config);
361
+ }
362
+
363
+ // ===========================
364
+ // CodexLens Embedding Rotation Management
365
+ // ===========================
366
+
367
+ /**
368
+ * Get CodexLens embedding rotation config
369
+ */
370
+ export function getCodexLensEmbeddingRotation(baseDir: string): CodexLensEmbeddingRotation | undefined {
371
+ const config = loadLiteLLMApiConfig(baseDir);
372
+ return config.codexlensEmbeddingRotation;
373
+ }
374
+
375
+ /**
376
+ * Update CodexLens embedding rotation config
377
+ * Also triggers sync to CodexLens settings.json
378
+ */
379
+ export function updateCodexLensEmbeddingRotation(
380
+ baseDir: string,
381
+ rotationConfig: CodexLensEmbeddingRotation | undefined
382
+ ): { syncResult: { success: boolean; message: string; endpointCount?: number } } {
383
+ const config = loadLiteLLMApiConfig(baseDir);
384
+
385
+ if (rotationConfig) {
386
+ config.codexlensEmbeddingRotation = rotationConfig;
387
+ } else {
388
+ delete config.codexlensEmbeddingRotation;
389
+ }
390
+
391
+ saveConfig(baseDir, config);
392
+
393
+ // Auto-sync to CodexLens settings.json
394
+ const syncResult = syncCodexLensConfig(baseDir);
395
+ return { syncResult };
396
+ }
397
+
398
+ /**
399
+ * Get all enabled embedding providers with their API keys for rotation
400
+ * This aggregates all providers that have embedding models configured
401
+ */
402
+ export function getEmbeddingProvidersForRotation(baseDir: string): Array<{
403
+ providerId: string;
404
+ providerName: string;
405
+ apiBase: string;
406
+ embeddingModels: Array<{
407
+ modelId: string;
408
+ modelName: string;
409
+ dimensions: number;
410
+ }>;
411
+ apiKeys: Array<{
412
+ keyId: string;
413
+ keyLabel: string;
414
+ enabled: boolean;
415
+ }>;
416
+ }> {
417
+ const config = loadLiteLLMApiConfig(baseDir);
418
+ const result: Array<{
419
+ providerId: string;
420
+ providerName: string;
421
+ apiBase: string;
422
+ embeddingModels: Array<{
423
+ modelId: string;
424
+ modelName: string;
425
+ dimensions: number;
426
+ }>;
427
+ apiKeys: Array<{
428
+ keyId: string;
429
+ keyLabel: string;
430
+ enabled: boolean;
431
+ }>;
432
+ }> = [];
433
+
434
+ for (const provider of config.providers) {
435
+ if (!provider.enabled) continue;
436
+
437
+ // Check if provider has embedding models
438
+ const embeddingModels = (provider.embeddingModels || [])
439
+ .filter(m => m.enabled)
440
+ .map(m => ({
441
+ modelId: m.id,
442
+ modelName: m.name,
443
+ dimensions: m.capabilities?.embeddingDimension || 1536,
444
+ }));
445
+
446
+ if (embeddingModels.length === 0) continue;
447
+
448
+ // Get API keys (single key or multiple from apiKeys array)
449
+ const apiKeys: Array<{ keyId: string; keyLabel: string; enabled: boolean }> = [];
450
+
451
+ if (provider.apiKeys && provider.apiKeys.length > 0) {
452
+ // Use multi-key configuration
453
+ for (const keyEntry of provider.apiKeys) {
454
+ apiKeys.push({
455
+ keyId: keyEntry.id,
456
+ keyLabel: keyEntry.label || keyEntry.id,
457
+ enabled: keyEntry.enabled,
458
+ });
459
+ }
460
+ } else if (provider.apiKey) {
461
+ // Single key fallback
462
+ apiKeys.push({
463
+ keyId: 'default',
464
+ keyLabel: 'Default Key',
465
+ enabled: true,
466
+ });
467
+ }
468
+
469
+ result.push({
470
+ providerId: provider.id,
471
+ providerName: provider.name,
472
+ apiBase: provider.apiBase || getDefaultApiBaseForType(provider.type),
473
+ embeddingModels,
474
+ apiKeys,
475
+ });
476
+ }
477
+
478
+ return result;
479
+ }
480
+
481
+ /**
482
+ * Generate rotation endpoints for ccw_litellm
483
+ * Creates endpoint list from rotation config for parallel embedding
484
+ * Supports both legacy codexlensEmbeddingRotation and new embeddingPoolConfig
485
+ */
486
+ export function generateRotationEndpoints(baseDir: string): Array<{
487
+ name: string;
488
+ api_key: string;
489
+ api_base: string;
490
+ model: string;
491
+ weight: number;
492
+ max_concurrent: number;
493
+ }> {
494
+ const config = loadLiteLLMApiConfig(baseDir);
495
+
496
+ // Prefer embeddingPoolConfig, fallback to codexlensEmbeddingRotation for backward compatibility
497
+ const poolConfig = config.embeddingPoolConfig;
498
+ const rotationConfig = config.codexlensEmbeddingRotation;
499
+
500
+ // Check if new poolConfig is enabled
501
+ if (poolConfig && poolConfig.enabled) {
502
+ return generateEndpointsFromPool(baseDir, poolConfig, config);
503
+ }
504
+
505
+ // Fallback to legacy rotation config
506
+ if (rotationConfig && rotationConfig.enabled) {
507
+ return generateEndpointsFromLegacyRotation(baseDir, rotationConfig, config);
508
+ }
509
+
510
+ return [];
511
+ }
512
+
513
+ /**
514
+ * Generate endpoints from new embeddingPoolConfig (with auto-discovery support)
515
+ */
516
+ function generateEndpointsFromPool(
517
+ baseDir: string,
518
+ poolConfig: EmbeddingPoolConfig,
519
+ config: LiteLLMApiConfig
520
+ ): Array<{
521
+ name: string;
522
+ api_key: string;
523
+ api_base: string;
524
+ model: string;
525
+ weight: number;
526
+ max_concurrent: number;
527
+ }> {
528
+ const endpoints: Array<{
529
+ name: string;
530
+ api_key: string;
531
+ api_base: string;
532
+ model: string;
533
+ weight: number;
534
+ max_concurrent: number;
535
+ }> = [];
536
+
537
+ if (poolConfig.autoDiscover) {
538
+ // Auto-discover all providers offering targetModel
539
+ const discovered = discoverProvidersForModel(baseDir, poolConfig.targetModel);
540
+ const excludedIds = new Set(poolConfig.excludedProviderIds || []);
541
+
542
+ for (const disc of discovered) {
543
+ // Skip excluded providers
544
+ if (excludedIds.has(disc.providerId)) continue;
545
+
546
+ // Find the provider config
547
+ const provider = config.providers.find(p => p.id === disc.providerId);
548
+ if (!provider || !provider.enabled) continue;
549
+
550
+ // Find the embedding model
551
+ const embeddingModel = provider.embeddingModels?.find(m => m.id === disc.modelId);
552
+ if (!embeddingModel || !embeddingModel.enabled) continue;
553
+
554
+ // Get API base (model-specific or provider default)
555
+ const apiBase = embeddingModel.endpointSettings?.baseUrl ||
556
+ provider.apiBase ||
557
+ getDefaultApiBaseForType(provider.type);
558
+
559
+ // Get API keys to use
560
+ let keysToUse: Array<{ id: string; key: string; label: string }> = [];
561
+
562
+ if (provider.apiKeys && provider.apiKeys.length > 0) {
563
+ // Use all enabled keys
564
+ keysToUse = provider.apiKeys
565
+ .filter(k => k.enabled)
566
+ .map(k => ({ id: k.id, key: k.key, label: k.label || k.id }));
567
+ } else if (provider.apiKey) {
568
+ // Single key fallback
569
+ keysToUse = [{ id: 'default', key: provider.apiKey, label: 'Default' }];
570
+ }
571
+
572
+ // Create endpoint for each key
573
+ for (const keyInfo of keysToUse) {
574
+ endpoints.push({
575
+ name: `${provider.name}-${keyInfo.label}`,
576
+ api_key: resolveEnvVar(keyInfo.key),
577
+ api_base: apiBase,
578
+ model: embeddingModel.name,
579
+ weight: 1.0, // Default weight for auto-discovered providers
580
+ max_concurrent: poolConfig.defaultMaxConcurrentPerKey,
581
+ });
582
+ }
583
+ }
584
+ }
585
+
586
+ return endpoints;
587
+ }
588
+
589
+ /**
590
+ * Generate endpoints from legacy codexlensEmbeddingRotation config
591
+ */
592
+ function generateEndpointsFromLegacyRotation(
593
+ baseDir: string,
594
+ rotationConfig: CodexLensEmbeddingRotation,
595
+ config: LiteLLMApiConfig
596
+ ): Array<{
597
+ name: string;
598
+ api_key: string;
599
+ api_base: string;
600
+ model: string;
601
+ weight: number;
602
+ max_concurrent: number;
603
+ }> {
604
+ const endpoints: Array<{
605
+ name: string;
606
+ api_key: string;
607
+ api_base: string;
608
+ model: string;
609
+ weight: number;
610
+ max_concurrent: number;
611
+ }> = [];
612
+
613
+ for (const rotationProvider of rotationConfig.providers) {
614
+ if (!rotationProvider.enabled) continue;
615
+
616
+ // Find the provider config
617
+ const provider = config.providers.find(p => p.id === rotationProvider.providerId);
618
+ if (!provider || !provider.enabled) continue;
619
+
620
+ // Find the embedding model
621
+ const embeddingModel = provider.embeddingModels?.find(m => m.id === rotationProvider.modelId);
622
+ if (!embeddingModel || !embeddingModel.enabled) continue;
623
+
624
+ // Get API base (model-specific or provider default)
625
+ const apiBase = embeddingModel.endpointSettings?.baseUrl ||
626
+ provider.apiBase ||
627
+ getDefaultApiBaseForType(provider.type);
628
+
629
+ // Get API keys to use
630
+ let keysToUse: Array<{ id: string; key: string; label: string }> = [];
631
+
632
+ if (provider.apiKeys && provider.apiKeys.length > 0) {
633
+ if (rotationProvider.useAllKeys) {
634
+ // Use all enabled keys
635
+ keysToUse = provider.apiKeys
636
+ .filter(k => k.enabled)
637
+ .map(k => ({ id: k.id, key: k.key, label: k.label || k.id }));
638
+ } else if (rotationProvider.selectedKeyIds && rotationProvider.selectedKeyIds.length > 0) {
639
+ // Use only selected keys
640
+ keysToUse = provider.apiKeys
641
+ .filter(k => k.enabled && rotationProvider.selectedKeyIds!.includes(k.id))
642
+ .map(k => ({ id: k.id, key: k.key, label: k.label || k.id }));
643
+ }
644
+ } else if (provider.apiKey) {
645
+ // Single key fallback
646
+ keysToUse = [{ id: 'default', key: provider.apiKey, label: 'Default' }];
647
+ }
648
+
649
+ // Create endpoint for each key
650
+ for (const keyInfo of keysToUse) {
651
+ endpoints.push({
652
+ name: `${provider.name}-${keyInfo.label}`,
653
+ api_key: resolveEnvVar(keyInfo.key),
654
+ api_base: apiBase,
655
+ model: embeddingModel.name,
656
+ weight: rotationProvider.weight,
657
+ max_concurrent: rotationProvider.maxConcurrentPerKey,
658
+ });
659
+ }
660
+ }
661
+
662
+ return endpoints;
663
+ }
664
+
665
+ /**
666
+ * Sync CodexLens settings with CCW API config
667
+ * Writes rotation endpoints to ~/.codexlens/settings.json
668
+ * This enables the Python backend to use UI-configured rotation
669
+ * Supports both new embeddingPoolConfig and legacy codexlensEmbeddingRotation
670
+ */
671
+ export function syncCodexLensConfig(baseDir: string): { success: boolean; message: string; endpointCount?: number } {
672
+ try {
673
+ const config = loadLiteLLMApiConfig(baseDir);
674
+
675
+ // Prefer embeddingPoolConfig, fallback to codexlensEmbeddingRotation
676
+ const poolConfig = config.embeddingPoolConfig;
677
+ const rotationConfig = config.codexlensEmbeddingRotation;
678
+
679
+ // Get CodexLens settings path
680
+ const codexlensDir = join(homedir(), '.codexlens');
681
+ const settingsPath = join(codexlensDir, 'settings.json');
682
+
683
+ // Ensure directory exists
684
+ if (!existsSync(codexlensDir)) {
685
+ mkdirSync(codexlensDir, { recursive: true });
686
+ }
687
+
688
+ // Load existing settings or create new
689
+ let settings: Record<string, unknown> = {};
690
+ if (existsSync(settingsPath)) {
691
+ try {
692
+ settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
693
+ } catch {
694
+ settings = {};
695
+ }
696
+ }
697
+
698
+ // Check if either config is enabled
699
+ const isPoolEnabled = poolConfig && poolConfig.enabled;
700
+ const isRotationEnabled = rotationConfig && rotationConfig.enabled;
701
+
702
+ // If neither is enabled, remove rotation endpoints and return
703
+ if (!isPoolEnabled && !isRotationEnabled) {
704
+ if (settings.litellm_rotation_endpoints) {
705
+ delete settings.litellm_rotation_endpoints;
706
+ delete settings.litellm_rotation_strategy;
707
+ delete settings.litellm_rotation_cooldown;
708
+ delete settings.litellm_target_model;
709
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
710
+ }
711
+ return { success: true, message: 'Rotation disabled, cleared endpoints', endpointCount: 0 };
712
+ }
713
+
714
+ // Generate rotation endpoints (function handles priority internally)
715
+ const endpoints = generateRotationEndpoints(baseDir);
716
+
717
+ if (endpoints.length === 0) {
718
+ return { success: false, message: 'No valid endpoints generated from rotation config' };
719
+ }
720
+
721
+ // Update settings with rotation config (use poolConfig if available)
722
+ settings.litellm_rotation_endpoints = endpoints;
723
+
724
+ if (isPoolEnabled) {
725
+ settings.litellm_rotation_strategy = poolConfig!.strategy;
726
+ settings.litellm_rotation_cooldown = poolConfig!.defaultCooldown;
727
+ settings.litellm_target_model = poolConfig!.targetModel;
728
+ } else {
729
+ settings.litellm_rotation_strategy = rotationConfig!.strategy;
730
+ settings.litellm_rotation_cooldown = rotationConfig!.defaultCooldown;
731
+ settings.litellm_target_model = rotationConfig!.targetModel;
732
+ }
733
+
734
+ // Write updated settings
735
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
736
+
737
+ return {
738
+ success: true,
739
+ message: `Synced ${endpoints.length} rotation endpoints to CodexLens`,
740
+ endpointCount: endpoints.length,
741
+ };
742
+ } catch (error) {
743
+ const errorMessage = error instanceof Error ? error.message : String(error);
744
+ console.error('[LiteLLM Config] Failed to sync CodexLens config:', errorMessage);
745
+ return { success: false, message: `Sync failed: ${errorMessage}` };
746
+ }
747
+ }
748
+
749
+ // ===========================
750
+ // Embedding Pool Management (Generic, with Auto-Discovery)
751
+ // ===========================
752
+
753
+ /**
754
+ * Get embedding pool config
755
+ */
756
+ export function getEmbeddingPoolConfig(baseDir: string): EmbeddingPoolConfig | undefined {
757
+ const config = loadLiteLLMApiConfig(baseDir);
758
+ return config.embeddingPoolConfig;
759
+ }
760
+
761
+ /**
762
+ * Update embedding pool config
763
+ * Also triggers sync to CodexLens settings.json if enabled
764
+ */
765
+ export function updateEmbeddingPoolConfig(
766
+ baseDir: string,
767
+ poolConfig: EmbeddingPoolConfig | undefined
768
+ ): { syncResult: { success: boolean; message: string; endpointCount?: number } } {
769
+ const config = loadLiteLLMApiConfig(baseDir);
770
+
771
+ if (poolConfig) {
772
+ config.embeddingPoolConfig = poolConfig;
773
+ } else {
774
+ delete config.embeddingPoolConfig;
775
+ }
776
+
777
+ saveConfig(baseDir, config);
778
+
779
+ // Auto-sync to CodexLens settings.json
780
+ const syncResult = syncCodexLensConfig(baseDir);
781
+ return { syncResult };
782
+ }
783
+
784
+ /**
785
+ * Discover all providers that offer a specific embedding model
786
+ * Returns list of {providerId, providerName, modelId, modelName, apiKeys[]}
787
+ */
788
+ export function discoverProvidersForModel(baseDir: string, targetModel: string): Array<{
789
+ providerId: string;
790
+ providerName: string;
791
+ modelId: string;
792
+ modelName: string;
793
+ apiKeys: Array<{ keyId: string; keyLabel: string; enabled: boolean }>;
794
+ }> {
795
+ const config = loadLiteLLMApiConfig(baseDir);
796
+ const result: Array<{
797
+ providerId: string;
798
+ providerName: string;
799
+ modelId: string;
800
+ modelName: string;
801
+ apiKeys: Array<{ keyId: string; keyLabel: string; enabled: boolean }>;
802
+ }> = [];
803
+
804
+ for (const provider of config.providers) {
805
+ if (!provider.enabled) continue;
806
+
807
+ // Check if provider has embedding models matching targetModel
808
+ const matchingModels = (provider.embeddingModels || []).filter(
809
+ m => m.enabled && (m.id === targetModel || m.name === targetModel)
810
+ );
811
+
812
+ if (matchingModels.length === 0) continue;
813
+
814
+ // Get API keys (single key or multiple from apiKeys array)
815
+ const apiKeys: Array<{ keyId: string; keyLabel: string; enabled: boolean }> = [];
816
+
817
+ if (provider.apiKeys && provider.apiKeys.length > 0) {
818
+ // Use multi-key configuration
819
+ for (const keyEntry of provider.apiKeys) {
820
+ apiKeys.push({
821
+ keyId: keyEntry.id,
822
+ keyLabel: keyEntry.label || keyEntry.id,
823
+ enabled: keyEntry.enabled,
824
+ });
825
+ }
826
+ } else if (provider.apiKey) {
827
+ // Single key fallback
828
+ apiKeys.push({
829
+ keyId: 'default',
830
+ keyLabel: 'Default Key',
831
+ enabled: true,
832
+ });
833
+ }
834
+
835
+ // Add each matching model
836
+ for (const model of matchingModels) {
837
+ result.push({
838
+ providerId: provider.id,
839
+ providerName: provider.name,
840
+ modelId: model.id,
841
+ modelName: model.name,
842
+ apiKeys,
843
+ });
844
+ }
845
+ }
846
+
847
+ return result;
848
+ }
849
+
850
+ // ===========================
851
+ // YAML Config Generation for ccw_litellm
852
+ // ===========================
853
+
854
+ /**
855
+ * Convert UI config (JSON) to ccw_litellm config (YAML format object)
856
+ * This allows CodexLens to use UI-configured providers
857
+ */
858
+ export function generateLiteLLMYamlConfig(baseDir: string): Record<string, unknown> {
859
+ const config = loadLiteLLMApiConfig(baseDir);
860
+
861
+ // Build providers object
862
+ const providers: Record<string, unknown> = {};
863
+ for (const provider of config.providers) {
864
+ if (!provider.enabled) continue;
865
+
866
+ providers[provider.id] = {
867
+ api_key: provider.apiKey,
868
+ api_base: provider.apiBase || getDefaultApiBaseForType(provider.type),
869
+ };
870
+ }
871
+
872
+ // Build embedding_models object from providers' embeddingModels
873
+ const embeddingModels: Record<string, unknown> = {};
874
+ for (const provider of config.providers) {
875
+ if (!provider.enabled || !provider.embeddingModels) continue;
876
+
877
+ for (const model of provider.embeddingModels) {
878
+ if (!model.enabled) continue;
879
+
880
+ embeddingModels[model.id] = {
881
+ provider: provider.id,
882
+ model: model.name,
883
+ dimensions: model.capabilities?.embeddingDimension || 1536,
884
+ // Use model-specific base URL if set, otherwise use provider's
885
+ ...(model.endpointSettings?.baseUrl && {
886
+ api_base: model.endpointSettings.baseUrl,
887
+ }),
888
+ };
889
+ }
890
+ }
891
+
892
+ // Build llm_models object from providers' llmModels
893
+ const llmModels: Record<string, unknown> = {};
894
+ for (const provider of config.providers) {
895
+ if (!provider.enabled || !provider.llmModels) continue;
896
+
897
+ for (const model of provider.llmModels) {
898
+ if (!model.enabled) continue;
899
+
900
+ llmModels[model.id] = {
901
+ provider: provider.id,
902
+ model: model.name,
903
+ ...(model.endpointSettings?.baseUrl && {
904
+ api_base: model.endpointSettings.baseUrl,
905
+ }),
906
+ };
907
+ }
908
+ }
909
+
910
+ // Find default provider
911
+ const defaultProvider = config.providers.find((p) => p.enabled)?.id || 'openai';
912
+
913
+ return {
914
+ version: 1,
915
+ default_provider: defaultProvider,
916
+ providers,
917
+ embedding_models: Object.keys(embeddingModels).length > 0 ? embeddingModels : {
918
+ default: {
919
+ provider: defaultProvider,
920
+ model: 'text-embedding-3-small',
921
+ dimensions: 1536,
922
+ },
923
+ },
924
+ llm_models: Object.keys(llmModels).length > 0 ? llmModels : {
925
+ default: {
926
+ provider: defaultProvider,
927
+ model: 'gpt-4',
928
+ },
929
+ },
930
+ };
931
+ }
932
+
933
+ /**
934
+ * Get default API base URL for provider type
935
+ */
936
+ function getDefaultApiBaseForType(type: ProviderType): string {
937
+ const defaults: Record<string, string> = {
938
+ openai: 'https://api.openai.com/v1',
939
+ anthropic: 'https://api.anthropic.com/v1',
940
+ custom: 'https://api.example.com/v1',
941
+ };
942
+ return defaults[type] || 'https://api.openai.com/v1';
943
+ }
944
+
945
+ /**
946
+ * Save ccw_litellm YAML config file
947
+ * Writes to ~/.ccw/config/litellm-config.yaml
948
+ */
949
+ export function saveLiteLLMYamlConfig(baseDir: string): string {
950
+ const yamlConfig = generateLiteLLMYamlConfig(baseDir);
951
+
952
+ // Convert to YAML manually (simple format)
953
+ const yamlContent = objectToYaml(yamlConfig);
954
+
955
+ // Write to ~/.ccw/config/litellm-config.yaml
956
+ const homePath = process.env.HOME || process.env.USERPROFILE || '';
957
+ const yamlPath = join(homePath, '.ccw', 'config', 'litellm-config.yaml');
958
+
959
+ // Ensure directory exists
960
+ const configDir = join(homePath, '.ccw', 'config');
961
+ ensureStorageDir(configDir);
962
+
963
+ writeFileSync(yamlPath, yamlContent, 'utf-8');
964
+ return yamlPath;
965
+ }
966
+
967
+ /**
968
+ * Simple object to YAML converter
969
+ */
970
+ function objectToYaml(obj: unknown, indent: number = 0): string {
971
+ const spaces = ' '.repeat(indent);
972
+
973
+ if (obj === null || obj === undefined) {
974
+ return 'null';
975
+ }
976
+
977
+ if (typeof obj === 'string') {
978
+ // Quote strings that contain special characters
979
+ if (obj.includes(':') || obj.includes('#') || obj.includes('\n') || obj.startsWith('$')) {
980
+ return `"${obj.replace(/"/g, '\\"')}"`;
981
+ }
982
+ return obj;
983
+ }
984
+
985
+ if (typeof obj === 'number' || typeof obj === 'boolean') {
986
+ return String(obj);
987
+ }
988
+
989
+ if (Array.isArray(obj)) {
990
+ if (obj.length === 0) return '[]';
991
+ return obj.map((item) => `${spaces}- ${objectToYaml(item, indent + 1).trimStart()}`).join('\n');
992
+ }
993
+
994
+ if (typeof obj === 'object') {
995
+ const entries = Object.entries(obj as Record<string, unknown>);
996
+ if (entries.length === 0) return '{}';
997
+
998
+ return entries
999
+ .map(([key, value]) => {
1000
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
1001
+ return `${spaces}${key}:\n${objectToYaml(value, indent + 1)}`;
1002
+ }
1003
+ return `${spaces}${key}: ${objectToYaml(value, indent)}`;
1004
+ })
1005
+ .join('\n');
1006
+ }
1007
+
1008
+ return String(obj);
1009
+ }
1010
+
1011
+ // Re-export types
1012
+ export type { ProviderCredential, CustomEndpoint, ProviderType, CacheStrategy, CodexLensEmbeddingRotation, CodexLensEmbeddingProvider, EmbeddingPoolConfig };