diff --git a/electron/gateway/manager.ts b/electron/gateway/manager.ts index 07068aec9..05533ce58 100644 --- a/electron/gateway/manager.ts +++ b/electron/gateway/manager.ts @@ -498,8 +498,14 @@ export class GatewayManager extends EventEmitter { if (pids.length > 0) { if (!this.process || !pids.includes(String(this.process.pid))) { logger.info(`Found orphaned process listening on port ${port} (PIDs: ${pids.join(', ')}), attempting to kill...`); + // SIGTERM first so the gateway can clean up its lock file. for (const pid of pids) { - try { process.kill(parseInt(pid), 'SIGKILL'); } catch { /* ignore */ } + try { process.kill(parseInt(pid), 'SIGTERM'); } catch { /* ignore */ } + } + await new Promise(r => setTimeout(r, 3000)); + // SIGKILL any survivors. + for (const pid of pids) { + try { process.kill(parseInt(pid), 0); process.kill(parseInt(pid), 'SIGKILL'); } catch { /* already exited */ } } await new Promise(r => setTimeout(r, 1000)); return null; diff --git a/electron/main/ipc-handlers.ts b/electron/main/ipc-handlers.ts index 615ec5248..f77a2d3e2 100644 --- a/electron/main/ipc-handlers.ts +++ b/electron/main/ipc-handlers.ts @@ -962,6 +962,8 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void { if (provider.type === 'custom' || provider.type === 'ollama') { // For runtime-configured providers, use user-entered base URL/api. + // Do NOT set apiKeyEnv — the OpenClaw gateway resolves custom + // provider keys via auth-profiles, not the config apiKey field. setOpenClawDefaultModelWithOverride(provider.type, modelOverride, { baseUrl: provider.baseUrl, api: 'openai-completions', diff --git a/electron/utils/openclaw-auth.ts b/electron/utils/openclaw-auth.ts index 840d0d0f7..01ccd639f 100644 --- a/electron/utils/openclaw-auth.ts +++ b/electron/utils/openclaw-auth.ts @@ -3,7 +3,7 @@ * Writes API keys to ~/.openclaw/agents/main/agent/auth-profiles.json * so the OpenClaw Gateway can load them for AI provider calls. */ -import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; +import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; import { @@ -81,51 +81,74 @@ function writeAuthProfiles(store: AuthProfilesStore, agentId = 'main'): void { writeFileSync(filePath, JSON.stringify(store, null, 2), 'utf-8'); } +/** + * Discover all agent IDs that have an agent/ subdirectory. + */ +function discoverAgentIds(): string[] { + const agentsDir = join(homedir(), '.openclaw', 'agents'); + try { + if (!existsSync(agentsDir)) return ['main']; + return readdirSync(agentsDir, { withFileTypes: true }) + .filter((d) => d.isDirectory() && existsSync(join(agentsDir, d.name, 'agent'))) + .map((d) => d.name); + } catch { + return ['main']; + } +} + /** * Save a provider API key to OpenClaw's auth-profiles.json * This writes the key in the format OpenClaw expects so the gateway * can use it for AI provider calls. + * + * Writes to ALL discovered agent directories so every agent + * (including non-"main" agents like "dev") stays in sync. * * @param provider - Provider type (e.g., 'anthropic', 'openrouter', 'openai', 'google') * @param apiKey - The API key to store - * @param agentId - Agent ID (defaults to 'main') + * @param agentId - Optional single agent ID. When omitted, writes to every agent. */ export function saveProviderKeyToOpenClaw( provider: string, apiKey: string, - agentId = 'main' + agentId?: string ): void { - const store = readAuthProfiles(agentId); - - // Profile ID follows OpenClaw convention: :default - const profileId = `${provider}:default`; - - // Upsert the profile entry - store.profiles[profileId] = { - type: 'api_key', - provider, - key: apiKey, - }; - - // Update order to include this profile - if (!store.order) { - store.order = {}; + const agentIds = agentId ? [agentId] : discoverAgentIds(); + if (agentIds.length === 0) agentIds.push('main'); + + for (const id of agentIds) { + const store = readAuthProfiles(id); + + // Profile ID follows OpenClaw convention: :default + const profileId = `${provider}:default`; + + // Upsert the profile entry + store.profiles[profileId] = { + type: 'api_key', + provider, + key: apiKey, + }; + + // Update order to include this profile + if (!store.order) { + store.order = {}; + } + if (!store.order[provider]) { + store.order[provider] = []; + } + if (!store.order[provider].includes(profileId)) { + store.order[provider].push(profileId); + } + + // Set as last good + if (!store.lastGood) { + store.lastGood = {}; + } + store.lastGood[provider] = profileId; + + writeAuthProfiles(store, id); } - if (!store.order[provider]) { - store.order[provider] = []; - } - if (!store.order[provider].includes(profileId)) { - store.order[provider].push(profileId); - } - - // Set as last good - if (!store.lastGood) { - store.lastGood = {}; - } - store.lastGood[provider] = profileId; - - writeAuthProfiles(store, agentId); - console.log(`Saved API key for provider "${provider}" to OpenClaw auth-profiles (agent: ${agentId})`); + console.log(`Saved API key for provider "${provider}" to OpenClaw auth-profiles (agents: ${agentIds.join(', ')})`); } /** @@ -133,26 +156,31 @@ export function saveProviderKeyToOpenClaw( */ export function removeProviderKeyFromOpenClaw( provider: string, - agentId = 'main' + agentId?: string ): void { - const store = readAuthProfiles(agentId); - const profileId = `${provider}:default`; + const agentIds = agentId ? [agentId] : discoverAgentIds(); + if (agentIds.length === 0) agentIds.push('main'); - delete store.profiles[profileId]; + for (const id of agentIds) { + const store = readAuthProfiles(id); + const profileId = `${provider}:default`; - if (store.order?.[provider]) { - store.order[provider] = store.order[provider].filter((id) => id !== profileId); - if (store.order[provider].length === 0) { - delete store.order[provider]; + delete store.profiles[profileId]; + + if (store.order?.[provider]) { + store.order[provider] = store.order[provider].filter((aid) => aid !== profileId); + if (store.order[provider].length === 0) { + delete store.order[provider]; + } } - } - if (store.lastGood?.[provider] === profileId) { - delete store.lastGood[provider]; - } + if (store.lastGood?.[provider] === profileId) { + delete store.lastGood[provider]; + } - writeAuthProfiles(store, agentId); - console.log(`Removed API key for provider "${provider}" from OpenClaw auth-profiles (agent: ${agentId})`); + writeAuthProfiles(store, id); + } + console.log(`Removed API key for provider "${provider}" from OpenClaw auth-profiles (agents: ${agentIds.join(', ')})`); } /** @@ -244,7 +272,7 @@ export function setOpenClawDefaultModel(provider: string, modelOverride?: string ...existingProvider, baseUrl: providerCfg.baseUrl, api: providerCfg.api, - apiKey: `\${${providerCfg.apiKeyEnv}}`, + apiKey: providerCfg.apiKeyEnv, models: mergedModels, }; console.log(`Configured models.providers.${provider} with baseUrl=${providerCfg.baseUrl}, model=${modelId}`); @@ -329,27 +357,22 @@ export function setOpenClawDefaultModelWithOverride( const models = (config.models || {}) as Record; const providers = (models.providers || {}) as Record; - const existingProvider = - providers[provider] && typeof providers[provider] === 'object' - ? (providers[provider] as Record) - : {}; - - const existingModels = Array.isArray(existingProvider.models) - ? (existingProvider.models as Array>) - : []; - const mergedModels = [...existingModels]; - if (modelId && !mergedModels.some((m) => m.id === modelId)) { - mergedModels.push({ id: modelId, name: modelId }); + // Replace the provider entry entirely rather than merging. + // Different custom/ollama provider instances have different baseUrls, + // so merging models from a previous instance creates an inconsistent + // config (models pointing at the wrong endpoint). + const nextModels: Array> = []; + if (modelId) { + nextModels.push({ id: modelId, name: modelId }); } const nextProvider: Record = { - ...existingProvider, baseUrl: override.baseUrl, api: override.api, - models: mergedModels, + models: nextModels, }; if (override.apiKeyEnv) { - nextProvider.apiKey = `\${${override.apiKeyEnv}}`; + nextProvider.apiKey = override.apiKeyEnv; } providers[provider] = nextProvider; diff --git a/electron/utils/provider-registry.ts b/electron/utils/provider-registry.ts index 155d87091..c746e63fd 100644 --- a/electron/utils/provider-registry.ts +++ b/electron/utils/provider-registry.ts @@ -52,7 +52,7 @@ const REGISTRY: Record = { }, google: { envVar: 'GEMINI_API_KEY', - defaultModel: 'google/gemini-3-pro-preview', + defaultModel: 'google/gemini-3.1-pro-preview', // google is built-in to OpenClaw's pi-ai catalog, no providerConfig needed. // Adding models.providers.google overrides the built-in and can break Gemini. }, @@ -94,6 +94,9 @@ const REGISTRY: Record = { apiKeyEnv: 'SILICONFLOW_API_KEY', }, }, + custom: { + envVar: 'CUSTOM_API_KEY', + }, // Additional providers with env var mappings but no default model groq: { envVar: 'GROQ_API_KEY' }, deepgram: { envVar: 'DEEPGRAM_API_KEY' },