From e8c11887d04b8a23f3dcdd748de2b6a74089492d Mon Sep 17 00:00:00 2001 From: Haze <709547807@qq.com> Date: Wed, 25 Feb 2026 18:56:04 +0800 Subject: [PATCH] =?UTF-8?q?feat(gateway):=20sync=20gateway=20token=20and?= =?UTF-8?q?=20update=20agent=20models=20on=20provider=E2=80=A6=20(#168)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/gateway/manager.ts | 12 ++++ electron/main/ipc-handlers.ts | 57 ++++++++++++++++ electron/utils/openclaw-auth.ts | 116 ++++++++++++++++++++++++++++++++ 3 files changed, 185 insertions(+) diff --git a/electron/gateway/manager.ts b/electron/gateway/manager.ts index cbe226bc9..87fb52bd8 100644 --- a/electron/gateway/manager.ts +++ b/electron/gateway/manager.ts @@ -29,6 +29,7 @@ import { buildDeviceAuthPayload, type DeviceIdentity, } from '../utils/device-identity'; +import { syncGatewayTokenToConfig } from '../utils/openclaw-auth'; /** * Gateway connection status @@ -559,6 +560,17 @@ export class GatewayManager extends EventEmitter { // Get or generate gateway token const gatewayToken = await getSetting('gatewayToken'); + + // Write our token into openclaw.json before starting the process. + // Without --dev the gateway authenticates using the token in + // openclaw.json; if that file has a stale token (e.g. left by the + // system-managed launchctl service) the WebSocket handshake will fail + // with "token mismatch" even though we pass --token on the CLI. + try { + syncGatewayTokenToConfig(gatewayToken); + } catch (err) { + logger.warn('Failed to sync gateway token to openclaw.json:', err); + } let command: string; let args: string[]; diff --git a/electron/main/ipc-handlers.ts b/electron/main/ipc-handlers.ts index f77a2d3e2..01aac9df5 100644 --- a/electron/main/ipc-handlers.ts +++ b/electron/main/ipc-handlers.ts @@ -30,6 +30,7 @@ import { removeProviderKeyFromOpenClaw, setOpenClawDefaultModel, setOpenClawDefaultModelWithOverride, + updateAgentModelProvider, } from '../utils/openclaw-auth'; import { logger } from '../utils/logger'; import { @@ -894,6 +895,43 @@ 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; + 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); + if (resolvedKey && nextConfig.baseUrl) { + const modelId = nextConfig.model; + updateAgentModelProvider(nextConfig.type, { + baseUrl: nextConfig.baseUrl, + api: 'openai-completions', + models: modelId ? [{ id: modelId, name: modelId }] : [], + apiKey: resolvedKey, + }); + } + } else { + setOpenClawDefaultModel(nextConfig.type, modelOverride); + } + } catch (err) { + console.warn('Failed to sync openclaw config after provider update:', err); + } + } + return { success: true }; } catch (error) { // Best-effort rollback to keep config/key consistent. @@ -979,6 +1017,25 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void { saveProviderKeyToOpenClaw(provider.type, providerKey); } + // 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 ( + (provider.type === 'custom' || provider.type === 'ollama') && + providerKey && + provider.baseUrl + ) { + const modelId = provider.model; + updateAgentModelProvider(provider.type, { + baseUrl: provider.baseUrl, + api: 'openai-completions', + models: modelId ? [{ id: modelId, name: modelId }] : [], + apiKey: providerKey, + }); + } + // 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. diff --git a/electron/utils/openclaw-auth.ts b/electron/utils/openclaw-auth.ts index 01ccd639f..b6f60952c 100644 --- a/electron/utils/openclaw-auth.ts +++ b/electron/utils/openclaw-auth.ts @@ -398,4 +398,120 @@ export function setOpenClawDefaultModelWithOverride( } // Re-export for backwards compatibility +/** + * Write the ClawX gateway token into ~/.openclaw/openclaw.json so the + * gateway process reads the same token we use for the WebSocket handshake. + * + * Without this, openclaw.json may contain a stale token written by the + * system-managed gateway service (launchctl), causing a "token mismatch" + * auth failure when ClawX connects to the process it just spawned. + */ +export function syncGatewayTokenToConfig(token: string): void { + const configPath = join(homedir(), '.openclaw', 'openclaw.json'); + let config: Record = {}; + try { + if (existsSync(configPath)) { + config = JSON.parse(readFileSync(configPath, 'utf-8')) as Record; + } + } catch { + // start from a blank config if the file is corrupt + } + + const gateway = ( + config.gateway && typeof config.gateway === 'object' + ? { ...(config.gateway as Record) } + : {} + ) as Record; + + const auth = ( + gateway.auth && typeof gateway.auth === 'object' + ? { ...(gateway.auth as Record) } + : {} + ) as Record; + + auth.mode = 'token'; + auth.token = token; + gateway.auth = auth; + if (!gateway.mode) gateway.mode = 'local'; + config.gateway = gateway; + + const dir = join(configPath, '..'); + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } + writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8'); + console.log('Synced gateway token to openclaw.json'); +} + +/** + * Update a provider entry in every discovered agent's models.json. + * + * The gateway caches resolved provider configs in + * ~/.openclaw/agents//agent/models.json and serves requests from + * that file (not from openclaw.json directly). We must update it + * whenever the active provider changes so the gateway immediately picks + * up the new baseUrl / apiKey without requiring a full restart. + * + * Existing model-level metadata (contextWindow, cost, etc.) is preserved + * when the model ID matches; only the top-level provider fields and the + * models list are updated. + */ +export function updateAgentModelProvider( + providerType: string, + entry: { + baseUrl?: string; + api?: string; + models?: Array<{ id: string; name: string }>; + apiKey?: string; + } +): void { + const agentIds = discoverAgentIds(); + for (const agentId of agentIds) { + const modelsPath = join(homedir(), '.openclaw', 'agents', agentId, 'agent', 'models.json'); + let data: Record = {}; + try { + if (existsSync(modelsPath)) { + data = JSON.parse(readFileSync(modelsPath, 'utf-8')) as Record; + } + } catch { + // corrupt / missing – start with an empty object + } + + const providers = ( + data.providers && typeof data.providers === 'object' ? data.providers : {} + ) as Record>; + + const existing: Record = + providers[providerType] && typeof providers[providerType] === 'object' + ? { ...providers[providerType] } + : {}; + + // Preserve per-model metadata (reasoning, cost, contextWindow…) for + // models that already exist; use a minimal stub for new models. + const existingModels = Array.isArray(existing.models) + ? (existing.models as Array>) + : []; + + const mergedModels = (entry.models ?? []).map((m) => { + const prev = existingModels.find((e) => e.id === m.id); + return prev ? { ...prev, id: m.id, name: m.name } : { ...m }; + }); + + if (entry.baseUrl !== undefined) existing.baseUrl = entry.baseUrl; + if (entry.api !== undefined) existing.api = entry.api; + if (mergedModels.length > 0) existing.models = mergedModels; + if (entry.apiKey !== undefined) existing.apiKey = entry.apiKey; + + providers[providerType] = existing; + data.providers = providers; + + try { + writeFileSync(modelsPath, JSON.stringify(data, null, 2), 'utf-8'); + console.log(`Updated models.json for agent "${agentId}" provider "${providerType}"`); + } catch (err) { + console.warn(`Failed to update models.json for agent "${agentId}":`, err); + } + } +} + export { getProviderEnvVar } from './provider-registry';