feat(gateway): sync gateway token and update agent models on provider… (#168)

This commit is contained in:
Haze
2026-02-25 18:56:04 +08:00
committed by GitHub
Unverified
parent 2804b6da73
commit e8c11887d0
3 changed files with 185 additions and 0 deletions

View File

@@ -29,6 +29,7 @@ import {
buildDeviceAuthPayload, buildDeviceAuthPayload,
type DeviceIdentity, type DeviceIdentity,
} from '../utils/device-identity'; } from '../utils/device-identity';
import { syncGatewayTokenToConfig } from '../utils/openclaw-auth';
/** /**
* Gateway connection status * Gateway connection status
@@ -559,6 +560,17 @@ export class GatewayManager extends EventEmitter {
// Get or generate gateway token // Get or generate gateway token
const gatewayToken = await getSetting('gatewayToken'); 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 command: string;
let args: string[]; let args: string[];

View File

@@ -30,6 +30,7 @@ import {
removeProviderKeyFromOpenClaw, removeProviderKeyFromOpenClaw,
setOpenClawDefaultModel, setOpenClawDefaultModel,
setOpenClawDefaultModelWithOverride, setOpenClawDefaultModelWithOverride,
updateAgentModelProvider,
} from '../utils/openclaw-auth'; } from '../utils/openclaw-auth';
import { logger } from '../utils/logger'; import { logger } from '../utils/logger';
import { 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 }; return { success: true };
} catch (error) { } catch (error) {
// Best-effort rollback to keep config/key consistent. // Best-effort rollback to keep config/key consistent.
@@ -979,6 +1017,25 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
saveProviderKeyToOpenClaw(provider.type, providerKey); 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. // Restart Gateway so it picks up the new config and env vars.
// OpenClaw reads openclaw.json per-request, but env vars (API keys) // OpenClaw reads openclaw.json per-request, but env vars (API keys)
// are only available if they were injected at process startup. // are only available if they were injected at process startup.

View File

@@ -398,4 +398,120 @@ export function setOpenClawDefaultModelWithOverride(
} }
// Re-export for backwards compatibility // 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<string, unknown> = {};
try {
if (existsSync(configPath)) {
config = JSON.parse(readFileSync(configPath, 'utf-8')) as Record<string, unknown>;
}
} catch {
// start from a blank config if the file is corrupt
}
const gateway = (
config.gateway && typeof config.gateway === 'object'
? { ...(config.gateway as Record<string, unknown>) }
: {}
) as Record<string, unknown>;
const auth = (
gateway.auth && typeof gateway.auth === 'object'
? { ...(gateway.auth as Record<string, unknown>) }
: {}
) as Record<string, unknown>;
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/<id>/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<string, unknown> = {};
try {
if (existsSync(modelsPath)) {
data = JSON.parse(readFileSync(modelsPath, 'utf-8')) as Record<string, unknown>;
}
} catch {
// corrupt / missing start with an empty object
}
const providers = (
data.providers && typeof data.providers === 'object' ? data.providers : {}
) as Record<string, Record<string, unknown>>;
const existing: Record<string, unknown> =
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<Record<string, unknown>>)
: [];
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'; export { getProviderEnvVar } from './provider-registry';