feat(gateway): sync gateway token and update agent models on provider… (#168)
This commit is contained in:
@@ -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[];
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<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';
|
||||
|
||||
Reference in New Issue
Block a user