From 7929a436013dd3062a6bae790b19c48862977436 Mon Sep 17 00:00:00 2001 From: paisley <8197966+su8su@users.noreply.github.com> Date: Thu, 26 Feb 2026 18:32:06 +0800 Subject: [PATCH] fix add model provider (#190) --- electron/main/ipc-handlers.ts | 126 ++++++++++++++++++++-------- electron/utils/openclaw-auth.ts | 60 +++++++++++++ electron/utils/provider-registry.ts | 12 +++ 3 files changed, 164 insertions(+), 34 deletions(-) diff --git a/electron/main/ipc-handlers.ts b/electron/main/ipc-handlers.ts index b0250c1db..791229ea4 100644 --- a/electron/main/ipc-handlers.ts +++ b/electron/main/ipc-handlers.ts @@ -30,6 +30,7 @@ import { removeProviderFromOpenClaw, setOpenClawDefaultModel, setOpenClawDefaultModelWithOverride, + syncProviderConfigToOpenClaw, updateAgentModelProvider, } from '../utils/openclaw-auth'; import { logger } from '../utils/logger'; @@ -832,17 +833,57 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void { await saveProvider(config); // Store the API key if provided - if (apiKey) { - await storeApiKey(config.id, apiKey); + if (apiKey !== undefined) { + const trimmedKey = apiKey.trim(); + if (trimmedKey) { + await storeApiKey(config.id, trimmedKey); - // Also write to OpenClaw auth-profiles.json so the gateway can use it - try { - saveProviderKeyToOpenClaw(config.type, apiKey); - } catch (err) { - console.warn('Failed to save key to OpenClaw auth-profiles:', err); + // Also write to OpenClaw auth-profiles.json so the gateway can use it + try { + saveProviderKeyToOpenClaw(config.type, trimmedKey); + } catch (err) { + console.warn('Failed to save key to OpenClaw auth-profiles:', err); + } } } + // Sync the provider configuration to openclaw.json so Gateway knows about it + try { + const meta = getProviderConfig(config.type); + const api = config.type === 'custom' || config.type === 'ollama' ? 'openai-completions' : meta?.api; + + if (api) { + syncProviderConfigToOpenClaw(config.type, config.model, { + baseUrl: config.baseUrl || meta?.baseUrl, + api, + apiKeyEnv: meta?.apiKeyEnv, + }); + + if (config.type === 'custom' || config.type === 'ollama') { + const resolvedKey = apiKey !== undefined + ? (apiKey.trim() || null) + : await getApiKey(config.id); + if (resolvedKey && config.baseUrl) { + const modelId = config.model; + updateAgentModelProvider(config.type, { + baseUrl: config.baseUrl, + api: 'openai-completions', + models: modelId ? [{ id: modelId, name: modelId }] : [], + apiKey: resolvedKey, + }); + } + } + + // Restart Gateway so it picks up the new config and env vars + logger.info(`Restarting Gateway after saving provider "${config.type}" config`); + void gatewayManager.restart().catch((err) => { + logger.warn('Gateway restart after provider save failed:', err); + }); + } + } catch (err) { + console.warn('Failed to sync openclaw provider config:', err); + } + return { success: true }; } catch (error) { return { success: false, error: String(error) }; @@ -928,26 +969,22 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void { } } - // If this provider is the current default, propagate model/baseUrl - // changes to openclaw.json and models.json immediately so the gateway - // picks them up without requiring the user to re-activate the provider. - const defaultProviderId = await getDefaultProvider(); - if (defaultProviderId === providerId) { - try { - const modelOverride = nextConfig.model - ? `${nextConfig.type}/${nextConfig.model}` - : undefined; + // Sync the provider configuration to openclaw.json so Gateway knows about it + try { + const meta = getProviderConfig(nextConfig.type); + const api = nextConfig.type === 'custom' || nextConfig.type === 'ollama' ? 'openai-completions' : meta?.api; + + if (api) { + syncProviderConfigToOpenClaw(nextConfig.type, nextConfig.model, { + baseUrl: nextConfig.baseUrl || meta?.baseUrl, + api, + apiKeyEnv: meta?.apiKeyEnv, + }); + if (nextConfig.type === 'custom' || nextConfig.type === 'ollama') { - setOpenClawDefaultModelWithOverride(nextConfig.type, modelOverride, { - baseUrl: nextConfig.baseUrl, - api: 'openai-completions', - }); - // Also update per-agent models.json so the gateway sees the - // change immediately (baseUrl or model ID may have changed). - const resolvedKey = - apiKey !== undefined - ? apiKey.trim() || null - : await getApiKey(providerId); + const resolvedKey = apiKey !== undefined + ? (apiKey.trim() || null) + : await getApiKey(providerId); if (resolvedKey && nextConfig.baseUrl) { const modelId = nextConfig.model; updateAgentModelProvider(nextConfig.type, { @@ -957,12 +994,32 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void { apiKey: resolvedKey, }); } - } else { - setOpenClawDefaultModel(nextConfig.type, modelOverride); } - } catch (err) { - console.warn('Failed to sync openclaw config after provider update:', err); } + + // If this provider is the current default, update the primary model + const defaultProviderId = await getDefaultProvider(); + if (defaultProviderId === providerId) { + const modelOverride = nextConfig.model + ? `${nextConfig.type}/${nextConfig.model}` + : undefined; + if (nextConfig.type !== 'custom' && nextConfig.type !== 'ollama') { + setOpenClawDefaultModel(nextConfig.type, modelOverride); + } else { + setOpenClawDefaultModelWithOverride(nextConfig.type, modelOverride, { + baseUrl: nextConfig.baseUrl, + api: 'openai-completions', + }); + } + } + + // Restart Gateway so it picks up the new config and env vars + logger.info(`Restarting Gateway after updating provider "${nextConfig.type}" config`); + void gatewayManager.restart().catch((err) => { + logger.warn('Gateway restart after provider update failed:', err); + }); + } catch (err) { + console.warn('Failed to sync openclaw config after provider update:', err); } return { success: true }; @@ -1027,11 +1084,12 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void { const provider = await getProvider(providerId); if (provider) { try { - // OAuth providers (qwen-portal, minimax-portal) have their openclaw.json - // model config already written by `openclaw models auth login --set-default`. - // Non-OAuth providers need us to write it here. + const providerKey = await getApiKey(providerId); + + // OAuth providers (qwen-portal, minimax-portal) might use OAuth OR a direct API key. + // Treat them as OAuth only if they don't have a local API key configured. const OAUTH_PROVIDER_TYPES = ['qwen-portal', 'minimax-portal']; - const isOAuthProvider = OAUTH_PROVIDER_TYPES.includes(provider.type); + const isOAuthProvider = OAUTH_PROVIDER_TYPES.includes(provider.type) && !providerKey; if (!isOAuthProvider) { // If the provider has a user-specified model (e.g. siliconflow), diff --git a/electron/utils/openclaw-auth.ts b/electron/utils/openclaw-auth.ts index b354495de..7c95834be 100644 --- a/electron/utils/openclaw-auth.ts +++ b/electron/utils/openclaw-auth.ts @@ -444,6 +444,66 @@ interface RuntimeProviderConfigOverride { apiKeyEnv?: string; } +/** + * Register or update a provider's configuration in openclaw.json + * without changing the current default model. + */ +export function syncProviderConfigToOpenClaw( + provider: string, + modelId: string | undefined, + override: RuntimeProviderConfigOverride +): void { + const configPath = join(homedir(), '.openclaw', 'openclaw.json'); + + let config: Record = {}; + try { + if (existsSync(configPath)) { + config = JSON.parse(readFileSync(configPath, 'utf-8')); + } + } catch (err) { + console.warn('Failed to read openclaw.json, creating fresh config:', err); + } + + if (override.baseUrl && override.api) { + const models = (config.models || {}) as Record; + const providers = (models.providers || {}) as Record; + + const nextModels: Array> = []; + if (modelId) { + nextModels.push({ id: modelId, name: modelId }); + } + + const nextProvider: Record = { + baseUrl: override.baseUrl, + api: override.api, + models: nextModels, + }; + if (override.apiKeyEnv) { + nextProvider.apiKey = override.apiKeyEnv; + } + + providers[provider] = nextProvider; + models.providers = providers; + config.models = models; + } + + // Ensure extension is enabled for oauth providers to prevent gateway wiping config + if (provider === 'minimax-portal' || provider === 'qwen-portal') { + const plugins = (config.plugins || {}) as Record; + const entries = (plugins.entries || {}) as Record; + entries[`${provider}-auth`] = { enabled: true }; + plugins.entries = entries; + config.plugins = plugins; + } + + const dir = join(configPath, '..'); + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } + + writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8'); +} + /** * Update OpenClaw model + provider config using runtime config values. * Useful for user-configurable providers (custom/ollama-like) where diff --git a/electron/utils/provider-registry.ts b/electron/utils/provider-registry.ts index 4b19270f4..fc6de6f82 100644 --- a/electron/utils/provider-registry.ts +++ b/electron/utils/provider-registry.ts @@ -97,10 +97,22 @@ const REGISTRY: Record = { }, }, 'minimax-portal': { + envVar: 'MINIMAX_API_KEY', defaultModel: 'minimax-portal/MiniMax-M2.1', + providerConfig: { + baseUrl: 'https://api.minimax.io/anthropic', + api: 'anthropic-messages', + apiKeyEnv: 'MINIMAX_API_KEY', + }, }, 'qwen-portal': { + envVar: 'QWEN_API_KEY', defaultModel: 'qwen-portal/coder-model', + providerConfig: { + baseUrl: 'https://portal.qwen.ai/v1', + api: 'openai-completions', + apiKeyEnv: 'QWEN_API_KEY', + }, }, custom: { envVar: 'CUSTOM_API_KEY',