|
3 | 3 | * Writes API keys to ~/.openclaw/agents/main/agent/auth-profiles.json |
4 | 4 | * so the OpenClaw Gateway can load them for AI provider calls. |
5 | 5 | */ |
6 | | -import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; |
| 6 | +import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'fs'; |
7 | 7 | import { join } from 'path'; |
8 | 8 | import { homedir } from 'os'; |
9 | 9 | import { |
@@ -81,78 +81,106 @@ function writeAuthProfiles(store: AuthProfilesStore, agentId = 'main'): void { |
81 | 81 | writeFileSync(filePath, JSON.stringify(store, null, 2), 'utf-8'); |
82 | 82 | } |
83 | 83 |
|
| 84 | +/** |
| 85 | + * Discover all agent IDs that have an agent/ subdirectory. |
| 86 | + */ |
| 87 | +function discoverAgentIds(): string[] { |
| 88 | + const agentsDir = join(homedir(), '.openclaw', 'agents'); |
| 89 | + try { |
| 90 | + if (!existsSync(agentsDir)) return ['main']; |
| 91 | + return readdirSync(agentsDir, { withFileTypes: true }) |
| 92 | + .filter((d) => d.isDirectory() && existsSync(join(agentsDir, d.name, 'agent'))) |
| 93 | + .map((d) => d.name); |
| 94 | + } catch { |
| 95 | + return ['main']; |
| 96 | + } |
| 97 | +} |
| 98 | + |
84 | 99 | /** |
85 | 100 | * Save a provider API key to OpenClaw's auth-profiles.json |
86 | 101 | * This writes the key in the format OpenClaw expects so the gateway |
87 | 102 | * can use it for AI provider calls. |
| 103 | + * |
| 104 | + * Writes to ALL discovered agent directories so every agent |
| 105 | + * (including non-"main" agents like "dev") stays in sync. |
88 | 106 | * |
89 | 107 | * @param provider - Provider type (e.g., 'anthropic', 'openrouter', 'openai', 'google') |
90 | 108 | * @param apiKey - The API key to store |
91 | | - * @param agentId - Agent ID (defaults to 'main') |
| 109 | + * @param agentId - Optional single agent ID. When omitted, writes to every agent. |
92 | 110 | */ |
93 | 111 | export function saveProviderKeyToOpenClaw( |
94 | 112 | provider: string, |
95 | 113 | apiKey: string, |
96 | | - agentId = 'main' |
| 114 | + agentId?: string |
97 | 115 | ): void { |
98 | | - const store = readAuthProfiles(agentId); |
99 | | - |
100 | | - // Profile ID follows OpenClaw convention: <provider>:default |
101 | | - const profileId = `${provider}:default`; |
102 | | - |
103 | | - // Upsert the profile entry |
104 | | - store.profiles[profileId] = { |
105 | | - type: 'api_key', |
106 | | - provider, |
107 | | - key: apiKey, |
108 | | - }; |
109 | | - |
110 | | - // Update order to include this profile |
111 | | - if (!store.order) { |
112 | | - store.order = {}; |
113 | | - } |
114 | | - if (!store.order[provider]) { |
115 | | - store.order[provider] = []; |
116 | | - } |
117 | | - if (!store.order[provider].includes(profileId)) { |
118 | | - store.order[provider].push(profileId); |
119 | | - } |
120 | | - |
121 | | - // Set as last good |
122 | | - if (!store.lastGood) { |
123 | | - store.lastGood = {}; |
| 116 | + const agentIds = agentId ? [agentId] : discoverAgentIds(); |
| 117 | + if (agentIds.length === 0) agentIds.push('main'); |
| 118 | + |
| 119 | + for (const id of agentIds) { |
| 120 | + const store = readAuthProfiles(id); |
| 121 | + |
| 122 | + // Profile ID follows OpenClaw convention: <provider>:default |
| 123 | + const profileId = `${provider}:default`; |
| 124 | + |
| 125 | + // Upsert the profile entry |
| 126 | + store.profiles[profileId] = { |
| 127 | + type: 'api_key', |
| 128 | + provider, |
| 129 | + key: apiKey, |
| 130 | + }; |
| 131 | + |
| 132 | + // Update order to include this profile |
| 133 | + if (!store.order) { |
| 134 | + store.order = {}; |
| 135 | + } |
| 136 | + if (!store.order[provider]) { |
| 137 | + store.order[provider] = []; |
| 138 | + } |
| 139 | + if (!store.order[provider].includes(profileId)) { |
| 140 | + store.order[provider].push(profileId); |
| 141 | + } |
| 142 | + |
| 143 | + // Set as last good |
| 144 | + if (!store.lastGood) { |
| 145 | + store.lastGood = {}; |
| 146 | + } |
| 147 | + store.lastGood[provider] = profileId; |
| 148 | + |
| 149 | + writeAuthProfiles(store, id); |
124 | 150 | } |
125 | | - store.lastGood[provider] = profileId; |
126 | | - |
127 | | - writeAuthProfiles(store, agentId); |
128 | | - console.log(`Saved API key for provider "${provider}" to OpenClaw auth-profiles (agent: ${agentId})`); |
| 151 | + console.log(`Saved API key for provider "${provider}" to OpenClaw auth-profiles (agents: ${agentIds.join(', ')})`); |
129 | 152 | } |
130 | 153 |
|
131 | 154 | /** |
132 | 155 | * Remove a provider API key from OpenClaw auth-profiles.json |
133 | 156 | */ |
134 | 157 | export function removeProviderKeyFromOpenClaw( |
135 | 158 | provider: string, |
136 | | - agentId = 'main' |
| 159 | + agentId?: string |
137 | 160 | ): void { |
138 | | - const store = readAuthProfiles(agentId); |
139 | | - const profileId = `${provider}:default`; |
| 161 | + const agentIds = agentId ? [agentId] : discoverAgentIds(); |
| 162 | + if (agentIds.length === 0) agentIds.push('main'); |
| 163 | + |
| 164 | + for (const id of agentIds) { |
| 165 | + const store = readAuthProfiles(id); |
| 166 | + const profileId = `${provider}:default`; |
140 | 167 |
|
141 | | - delete store.profiles[profileId]; |
| 168 | + delete store.profiles[profileId]; |
142 | 169 |
|
143 | | - if (store.order?.[provider]) { |
144 | | - store.order[provider] = store.order[provider].filter((id) => id !== profileId); |
145 | | - if (store.order[provider].length === 0) { |
146 | | - delete store.order[provider]; |
| 170 | + if (store.order?.[provider]) { |
| 171 | + store.order[provider] = store.order[provider].filter((aid) => aid !== profileId); |
| 172 | + if (store.order[provider].length === 0) { |
| 173 | + delete store.order[provider]; |
| 174 | + } |
147 | 175 | } |
148 | | - } |
149 | 176 |
|
150 | | - if (store.lastGood?.[provider] === profileId) { |
151 | | - delete store.lastGood[provider]; |
152 | | - } |
| 177 | + if (store.lastGood?.[provider] === profileId) { |
| 178 | + delete store.lastGood[provider]; |
| 179 | + } |
153 | 180 |
|
154 | | - writeAuthProfiles(store, agentId); |
155 | | - console.log(`Removed API key for provider "${provider}" from OpenClaw auth-profiles (agent: ${agentId})`); |
| 181 | + writeAuthProfiles(store, id); |
| 182 | + } |
| 183 | + console.log(`Removed API key for provider "${provider}" from OpenClaw auth-profiles (agents: ${agentIds.join(', ')})`); |
156 | 184 | } |
157 | 185 |
|
158 | 186 | /** |
@@ -244,7 +272,7 @@ export function setOpenClawDefaultModel(provider: string, modelOverride?: string |
244 | 272 | ...existingProvider, |
245 | 273 | baseUrl: providerCfg.baseUrl, |
246 | 274 | api: providerCfg.api, |
247 | | - apiKey: `\${${providerCfg.apiKeyEnv}}`, |
| 275 | + apiKey: providerCfg.apiKeyEnv, |
248 | 276 | models: mergedModels, |
249 | 277 | }; |
250 | 278 | console.log(`Configured models.providers.${provider} with baseUrl=${providerCfg.baseUrl}, model=${modelId}`); |
@@ -329,27 +357,22 @@ export function setOpenClawDefaultModelWithOverride( |
329 | 357 | const models = (config.models || {}) as Record<string, unknown>; |
330 | 358 | const providers = (models.providers || {}) as Record<string, unknown>; |
331 | 359 |
|
332 | | - const existingProvider = |
333 | | - providers[provider] && typeof providers[provider] === 'object' |
334 | | - ? (providers[provider] as Record<string, unknown>) |
335 | | - : {}; |
336 | | - |
337 | | - const existingModels = Array.isArray(existingProvider.models) |
338 | | - ? (existingProvider.models as Array<Record<string, unknown>>) |
339 | | - : []; |
340 | | - const mergedModels = [...existingModels]; |
341 | | - if (modelId && !mergedModels.some((m) => m.id === modelId)) { |
342 | | - mergedModels.push({ id: modelId, name: modelId }); |
| 360 | + // Replace the provider entry entirely rather than merging. |
| 361 | + // Different custom/ollama provider instances have different baseUrls, |
| 362 | + // so merging models from a previous instance creates an inconsistent |
| 363 | + // config (models pointing at the wrong endpoint). |
| 364 | + const nextModels: Array<Record<string, unknown>> = []; |
| 365 | + if (modelId) { |
| 366 | + nextModels.push({ id: modelId, name: modelId }); |
343 | 367 | } |
344 | 368 |
|
345 | 369 | const nextProvider: Record<string, unknown> = { |
346 | | - ...existingProvider, |
347 | 370 | baseUrl: override.baseUrl, |
348 | 371 | api: override.api, |
349 | | - models: mergedModels, |
| 372 | + models: nextModels, |
350 | 373 | }; |
351 | 374 | if (override.apiKeyEnv) { |
352 | | - nextProvider.apiKey = `\${${override.apiKeyEnv}}`; |
| 375 | + nextProvider.apiKey = override.apiKeyEnv; |
353 | 376 | } |
354 | 377 |
|
355 | 378 | providers[provider] = nextProvider; |
|
0 commit comments