fix: custom providers overwrite each other in openclaw config (#192)

This commit is contained in:
paisley
2026-02-26 18:55:40 +08:00
committed by GitHub
Unverified
parent 0f3505661d
commit efe091b301

View File

@@ -50,6 +50,24 @@ import { whatsAppLoginManager } from '../utils/whatsapp-login';
import { getProviderConfig } from '../utils/provider-registry'; import { getProviderConfig } from '../utils/provider-registry';
import { deviceOAuthManager, OAuthProviderType } from '../utils/device-oauth'; import { deviceOAuthManager, OAuthProviderType } from '../utils/device-oauth';
/**
* For custom/ollama providers, derive a unique key for OpenClaw config files
* so that multiple instances of the same type don't overwrite each other.
* For all other providers the key is simply the provider type.
*
* @param type - Provider type (e.g. 'custom', 'ollama', 'openrouter')
* @param providerId - Unique provider ID from secure-storage (UUID-like)
* @returns A string like 'custom-a1b2c3d4' or 'openrouter'
*/
function getOpenClawProviderKey(type: string, providerId: string): string {
if (type === 'custom' || type === 'ollama') {
// Use the first 8 chars of the providerId as a stable short suffix
const suffix = providerId.replace(/-/g, '').slice(0, 8);
return `${type}-${suffix}`;
}
return type;
}
/** /**
* Register all IPC handlers * Register all IPC handlers
*/ */
@@ -832,6 +850,9 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
// Save the provider config // Save the provider config
await saveProvider(config); await saveProvider(config);
// Derive the unique OpenClaw key for this provider instance
const ock = getOpenClawProviderKey(config.type, config.id);
// Store the API key if provided // Store the API key if provided
if (apiKey !== undefined) { if (apiKey !== undefined) {
const trimmedKey = apiKey.trim(); const trimmedKey = apiKey.trim();
@@ -840,7 +861,7 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
// Also write to OpenClaw auth-profiles.json so the gateway can use it // Also write to OpenClaw auth-profiles.json so the gateway can use it
try { try {
saveProviderKeyToOpenClaw(config.type, trimmedKey); saveProviderKeyToOpenClaw(ock, trimmedKey);
} catch (err) { } catch (err) {
console.warn('Failed to save key to OpenClaw auth-profiles:', err); console.warn('Failed to save key to OpenClaw auth-profiles:', err);
} }
@@ -853,7 +874,7 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
const api = config.type === 'custom' || config.type === 'ollama' ? 'openai-completions' : meta?.api; const api = config.type === 'custom' || config.type === 'ollama' ? 'openai-completions' : meta?.api;
if (api) { if (api) {
syncProviderConfigToOpenClaw(config.type, config.model, { syncProviderConfigToOpenClaw(ock, config.model, {
baseUrl: config.baseUrl || meta?.baseUrl, baseUrl: config.baseUrl || meta?.baseUrl,
api, api,
apiKeyEnv: meta?.apiKeyEnv, apiKeyEnv: meta?.apiKeyEnv,
@@ -865,7 +886,7 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
: await getApiKey(config.id); : await getApiKey(config.id);
if (resolvedKey && config.baseUrl) { if (resolvedKey && config.baseUrl) {
const modelId = config.model; const modelId = config.model;
updateAgentModelProvider(config.type, { updateAgentModelProvider(ock, {
baseUrl: config.baseUrl, baseUrl: config.baseUrl,
api: 'openai-completions', api: 'openai-completions',
models: modelId ? [{ id: modelId, name: modelId }] : [], models: modelId ? [{ id: modelId, name: modelId }] : [],
@@ -875,7 +896,7 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
} }
// Restart Gateway so it picks up the new config and env vars // Restart Gateway so it picks up the new config and env vars
logger.info(`Restarting Gateway after saving provider "${config.type}" config`); logger.info(`Restarting Gateway after saving provider "${ock}" config`);
void gatewayManager.restart().catch((err) => { void gatewayManager.restart().catch((err) => {
logger.warn('Gateway restart after provider save failed:', err); logger.warn('Gateway restart after provider save failed:', err);
}); });
@@ -899,7 +920,8 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
// Best-effort cleanup in OpenClaw auth profiles & openclaw.json config // Best-effort cleanup in OpenClaw auth profiles & openclaw.json config
if (existing?.type) { if (existing?.type) {
try { try {
removeProviderFromOpenClaw(existing.type); const ock = getOpenClawProviderKey(existing.type, providerId);
removeProviderFromOpenClaw(ock);
} catch (err) { } catch (err) {
console.warn('Failed to completely remove provider from OpenClaw:', err); console.warn('Failed to completely remove provider from OpenClaw:', err);
} }
@@ -917,11 +939,11 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
await storeApiKey(providerId, apiKey); await storeApiKey(providerId, apiKey);
// Also write to OpenClaw auth-profiles.json // Also write to OpenClaw auth-profiles.json
// Resolve provider type from stored config, or use providerId as type
const provider = await getProvider(providerId); const provider = await getProvider(providerId);
const providerType = provider?.type || providerId; const providerType = provider?.type || providerId;
const ock = getOpenClawProviderKey(providerType, providerId);
try { try {
saveProviderKeyToOpenClaw(providerType, apiKey); saveProviderKeyToOpenClaw(ock, apiKey);
} catch (err) { } catch (err) {
console.warn('Failed to save key to OpenClaw auth-profiles:', err); console.warn('Failed to save key to OpenClaw auth-profiles:', err);
} }
@@ -947,7 +969,7 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
} }
const previousKey = await getApiKey(providerId); const previousKey = await getApiKey(providerId);
const previousProviderType = existing.type; const previousOck = getOpenClawProviderKey(existing.type, providerId);
try { try {
const nextConfig: ProviderConfig = { const nextConfig: ProviderConfig = {
@@ -956,16 +978,18 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
}; };
const ock = getOpenClawProviderKey(nextConfig.type, providerId);
await saveProvider(nextConfig); await saveProvider(nextConfig);
if (apiKey !== undefined) { if (apiKey !== undefined) {
const trimmedKey = apiKey.trim(); const trimmedKey = apiKey.trim();
if (trimmedKey) { if (trimmedKey) {
await storeApiKey(providerId, trimmedKey); await storeApiKey(providerId, trimmedKey);
saveProviderKeyToOpenClaw(nextConfig.type, trimmedKey); saveProviderKeyToOpenClaw(ock, trimmedKey);
} else { } else {
await deleteApiKey(providerId); await deleteApiKey(providerId);
removeProviderFromOpenClaw(nextConfig.type); removeProviderFromOpenClaw(ock);
} }
} }
@@ -975,7 +999,7 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
const api = nextConfig.type === 'custom' || nextConfig.type === 'ollama' ? 'openai-completions' : meta?.api; const api = nextConfig.type === 'custom' || nextConfig.type === 'ollama' ? 'openai-completions' : meta?.api;
if (api) { if (api) {
syncProviderConfigToOpenClaw(nextConfig.type, nextConfig.model, { syncProviderConfigToOpenClaw(ock, nextConfig.model, {
baseUrl: nextConfig.baseUrl || meta?.baseUrl, baseUrl: nextConfig.baseUrl || meta?.baseUrl,
api, api,
apiKeyEnv: meta?.apiKeyEnv, apiKeyEnv: meta?.apiKeyEnv,
@@ -987,7 +1011,7 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
: await getApiKey(providerId); : await getApiKey(providerId);
if (resolvedKey && nextConfig.baseUrl) { if (resolvedKey && nextConfig.baseUrl) {
const modelId = nextConfig.model; const modelId = nextConfig.model;
updateAgentModelProvider(nextConfig.type, { updateAgentModelProvider(ock, {
baseUrl: nextConfig.baseUrl, baseUrl: nextConfig.baseUrl,
api: 'openai-completions', api: 'openai-completions',
models: modelId ? [{ id: modelId, name: modelId }] : [], models: modelId ? [{ id: modelId, name: modelId }] : [],
@@ -1001,12 +1025,12 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
const defaultProviderId = await getDefaultProvider(); const defaultProviderId = await getDefaultProvider();
if (defaultProviderId === providerId) { if (defaultProviderId === providerId) {
const modelOverride = nextConfig.model const modelOverride = nextConfig.model
? `${nextConfig.type}/${nextConfig.model}` ? `${ock}/${nextConfig.model}`
: undefined; : undefined;
if (nextConfig.type !== 'custom' && nextConfig.type !== 'ollama') { if (nextConfig.type !== 'custom' && nextConfig.type !== 'ollama') {
setOpenClawDefaultModel(nextConfig.type, modelOverride); setOpenClawDefaultModel(nextConfig.type, modelOverride);
} else { } else {
setOpenClawDefaultModelWithOverride(nextConfig.type, modelOverride, { setOpenClawDefaultModelWithOverride(ock, modelOverride, {
baseUrl: nextConfig.baseUrl, baseUrl: nextConfig.baseUrl,
api: 'openai-completions', api: 'openai-completions',
}); });
@@ -1014,7 +1038,7 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
} }
// Restart Gateway so it picks up the new config and env vars // Restart Gateway so it picks up the new config and env vars
logger.info(`Restarting Gateway after updating provider "${nextConfig.type}" config`); logger.info(`Restarting Gateway after updating provider "${ock}" config`);
void gatewayManager.restart().catch((err) => { void gatewayManager.restart().catch((err) => {
logger.warn('Gateway restart after provider update failed:', err); logger.warn('Gateway restart after provider update failed:', err);
}); });
@@ -1029,10 +1053,10 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
await saveProvider(existing); await saveProvider(existing);
if (previousKey) { if (previousKey) {
await storeApiKey(providerId, previousKey); await storeApiKey(providerId, previousKey);
saveProviderKeyToOpenClaw(previousProviderType, previousKey); saveProviderKeyToOpenClaw(previousOck, previousKey);
} else { } else {
await deleteApiKey(providerId); await deleteApiKey(providerId);
removeProviderFromOpenClaw(previousProviderType); removeProviderFromOpenClaw(previousOck);
} }
} catch (rollbackError) { } catch (rollbackError) {
console.warn('Failed to rollback provider updateWithKey:', rollbackError); console.warn('Failed to rollback provider updateWithKey:', rollbackError);
@@ -1051,9 +1075,10 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
// Keep OpenClaw auth-profiles.json in sync with local key storage // Keep OpenClaw auth-profiles.json in sync with local key storage
const provider = await getProvider(providerId); const provider = await getProvider(providerId);
const providerType = provider?.type || providerId; const providerType = provider?.type || providerId;
const ock = getOpenClawProviderKey(providerType, providerId);
try { try {
if (providerType) { if (ock) {
removeProviderFromOpenClaw(providerType); removeProviderFromOpenClaw(ock);
} }
} catch (err) { } catch (err) {
console.warn('Failed to completely remove provider from OpenClaw:', err); console.warn('Failed to completely remove provider from OpenClaw:', err);
@@ -1084,6 +1109,7 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
const provider = await getProvider(providerId); const provider = await getProvider(providerId);
if (provider) { if (provider) {
try { try {
const ock = getOpenClawProviderKey(provider.type, providerId);
const providerKey = await getApiKey(providerId); const providerKey = await getApiKey(providerId);
// OAuth providers (qwen-portal, minimax-portal) might use OAuth OR a direct API key. // OAuth providers (qwen-portal, minimax-portal) might use OAuth OR a direct API key.
@@ -1092,21 +1118,15 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
const isOAuthProvider = OAUTH_PROVIDER_TYPES.includes(provider.type) && !providerKey; const isOAuthProvider = OAUTH_PROVIDER_TYPES.includes(provider.type) && !providerKey;
if (!isOAuthProvider) { if (!isOAuthProvider) {
// If the provider has a user-specified model (e.g. siliconflow), // Build the full model string: "openclawKey/modelId"
// build the full model string: "providerType/modelId"
// Guard against double-prefixing: provider.model may already
// include the provider type (e.g. "siliconflow/DeepSeek-V3").
const modelOverride = provider.model const modelOverride = provider.model
? (provider.model.startsWith(`${provider.type}/`) ? (provider.model.startsWith(`${ock}/`)
? provider.model ? provider.model
: `${provider.type}/${provider.model}`) : `${ock}/${provider.model}`)
: undefined; : undefined;
if (provider.type === 'custom' || provider.type === 'ollama') { if (provider.type === 'custom' || provider.type === 'ollama') {
// For runtime-configured providers, use user-entered base URL/api. setOpenClawDefaultModelWithOverride(ock, modelOverride, {
// 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, baseUrl: provider.baseUrl,
api: 'openai-completions', api: 'openai-completions',
}); });
@@ -1115,15 +1135,11 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
} }
// Keep auth-profiles in sync with the default provider instance. // Keep auth-profiles in sync with the default provider instance.
// This is especially important when multiple custom providers exist.
const providerKey = await getApiKey(providerId);
if (providerKey) { if (providerKey) {
saveProviderKeyToOpenClaw(provider.type, providerKey); saveProviderKeyToOpenClaw(ock, providerKey);
} }
} else { } else {
// OAuth providers (minimax-portal, qwen-portal): write the provider config // OAuth providers (minimax-portal, qwen-portal)
// using the model and baseUrl stored by device-oauth.ts at login time.
// These providers use their own API format (not standard OpenAI completions).
const defaultBaseUrl = provider.type === 'minimax-portal' const defaultBaseUrl = provider.type === 'minimax-portal'
? 'https://api.minimax.io/anthropic' ? 'https://api.minimax.io/anthropic'
: 'https://portal.qwen.ai/v1'; : 'https://portal.qwen.ai/v1';
@@ -1139,8 +1155,6 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
setOpenClawDefaultModelWithOverride(provider.type, undefined, { setOpenClawDefaultModelWithOverride(provider.type, undefined, {
baseUrl, baseUrl,
api, api,
// OAuth placeholder — Gateway uses this to look up OAuth credentials
// from auth-profiles.json instead of a static API key.
apiKeyEnv: provider.type === 'minimax-portal' ? 'minimax-oauth' : 'qwen-oauth', apiKeyEnv: provider.type === 'minimax-portal' ? 'minimax-oauth' : 'qwen-oauth',
}); });
@@ -1148,17 +1162,13 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
} }
// For custom/ollama providers, also update the per-agent models.json // For custom/ollama providers, also update the per-agent models.json
// so the gateway picks up the new baseUrl + key immediately.
// The gateway caches provider configs in models.json and reads from
// there at request time; updating openclaw.json alone is not enough
// when switching between multiple custom provider instances.
if ( if (
(provider.type === 'custom' || provider.type === 'ollama') && (provider.type === 'custom' || provider.type === 'ollama') &&
providerKey && providerKey &&
provider.baseUrl provider.baseUrl
) { ) {
const modelId = provider.model; const modelId = provider.model;
updateAgentModelProvider(provider.type, { updateAgentModelProvider(ock, {
baseUrl: provider.baseUrl, baseUrl: provider.baseUrl,
api: 'openai-completions', api: 'openai-completions',
models: modelId ? [{ id: modelId, name: modelId }] : [], models: modelId ? [{ id: modelId, name: modelId }] : [],
@@ -1167,10 +1177,8 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
} }
// Restart Gateway so it picks up the new config and env vars. // Restart Gateway so it picks up the new config and env vars.
// OpenClaw reads openclaw.json per-request, but env vars (API keys)
// are only available if they were injected at process startup.
if (gatewayManager.isConnected()) { if (gatewayManager.isConnected()) {
logger.info(`Restarting Gateway after provider switch to "${provider.type}"`); logger.info(`Restarting Gateway after provider switch to "${ock}"`);
void gatewayManager.restart().catch((err) => { void gatewayManager.restart().catch((err) => {
logger.warn('Gateway restart after provider switch failed:', err); logger.warn('Gateway restart after provider switch failed:', err);
}); });