feat(gateway): sync gateway token and update agent models on provider… (#168)
This commit is contained in:
@@ -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
|
||||||
@@ -560,6 +561,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[];
|
||||||
let mode: 'packaged' | 'dev-built' | 'dev-pnpm';
|
let mode: 'packaged' | 'dev-built' | 'dev-pnpm';
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user