refactor/channel & ipc (#349)

Co-authored-by: paisley <8197966+su8su@users.noreply.github.com>
Co-authored-by: zuolingxuan <zuolingxuan@bytedance.com>
This commit is contained in:
Lingxuan Zuo
2026-03-09 19:04:00 +08:00
committed by GitHub
Unverified
parent 8b45960662
commit e28eba01e1
47 changed files with 4160 additions and 543 deletions

View File

@@ -1,15 +1,5 @@
import type { IncomingMessage, ServerResponse } from 'http';
import {
deleteApiKey,
deleteProvider,
getAllProvidersWithKeyInfo,
getApiKey,
getDefaultProvider,
getProvider,
hasApiKey,
saveProvider,
setDefaultProvider,
storeApiKey,
type ProviderConfig,
} from '../../utils/secure-storage';
import {
@@ -135,19 +125,19 @@ export async function handleProviderRoutes(
}
if (url.pathname === '/api/providers' && req.method === 'GET') {
sendJson(res, 200, await getAllProvidersWithKeyInfo());
sendJson(res, 200, await providerService.listLegacyProvidersWithKeyInfo());
return true;
}
if (url.pathname === '/api/providers/default' && req.method === 'GET') {
sendJson(res, 200, { providerId: await getDefaultProvider() ?? null });
sendJson(res, 200, { providerId: await providerService.getDefaultLegacyProvider() ?? null });
return true;
}
if (url.pathname === '/api/providers/default' && req.method === 'PUT') {
try {
const body = await parseJsonBody<{ providerId: string }>(req);
await setDefaultProvider(body.providerId);
await providerService.setDefaultLegacyProvider(body.providerId);
await syncDefaultProviderToRuntime(body.providerId, ctx.gatewayManager);
sendJson(res, 200, { success: true });
} catch (error) {
@@ -159,7 +149,7 @@ export async function handleProviderRoutes(
if (url.pathname === '/api/providers/validate' && req.method === 'POST') {
try {
const body = await parseJsonBody<{ providerId: string; apiKey: string; options?: { baseUrl?: string } }>(req);
const provider = await getProvider(body.providerId);
const provider = await providerService.getLegacyProvider(body.providerId);
const providerType = provider?.type || body.providerId;
const registryBaseUrl = getProviderConfig(providerType)?.baseUrl;
const resolvedBaseUrl = body.options?.baseUrl || provider?.baseUrl || registryBaseUrl;
@@ -211,11 +201,11 @@ export async function handleProviderRoutes(
try {
const body = await parseJsonBody<{ config: ProviderConfig; apiKey?: string }>(req);
const config = body.config;
await saveProvider(config);
await providerService.saveLegacyProvider(config);
if (body.apiKey !== undefined) {
const trimmedKey = body.apiKey.trim();
if (trimmedKey) {
await storeApiKey(config.id, trimmedKey);
await providerService.setLegacyProviderApiKey(config.id, trimmedKey);
await syncProviderApiKeyToRuntime(config.type, config.id, trimmedKey);
}
}
@@ -231,15 +221,15 @@ export async function handleProviderRoutes(
const providerId = decodeURIComponent(url.pathname.slice('/api/providers/'.length));
if (providerId.endsWith('/api-key')) {
const actualId = providerId.slice(0, -('/api-key'.length));
sendJson(res, 200, { apiKey: await getApiKey(actualId) });
sendJson(res, 200, { apiKey: await providerService.getLegacyProviderApiKey(actualId) });
return true;
}
if (providerId.endsWith('/has-api-key')) {
const actualId = providerId.slice(0, -('/has-api-key'.length));
sendJson(res, 200, { hasKey: await hasApiKey(actualId) });
sendJson(res, 200, { hasKey: await providerService.hasLegacyProviderApiKey(actualId) });
return true;
}
sendJson(res, 200, await getProvider(providerId));
sendJson(res, 200, await providerService.getLegacyProvider(providerId));
return true;
}
@@ -247,20 +237,20 @@ export async function handleProviderRoutes(
const providerId = decodeURIComponent(url.pathname.slice('/api/providers/'.length));
try {
const body = await parseJsonBody<{ updates: Partial<ProviderConfig>; apiKey?: string }>(req);
const existing = await getProvider(providerId);
const existing = await providerService.getLegacyProvider(providerId);
if (!existing) {
sendJson(res, 404, { success: false, error: 'Provider not found' });
return true;
}
const nextConfig: ProviderConfig = { ...existing, ...body.updates, updatedAt: new Date().toISOString() };
await saveProvider(nextConfig);
await providerService.saveLegacyProvider(nextConfig);
if (body.apiKey !== undefined) {
const trimmedKey = body.apiKey.trim();
if (trimmedKey) {
await storeApiKey(providerId, trimmedKey);
await providerService.setLegacyProviderApiKey(providerId, trimmedKey);
await syncProviderApiKeyToRuntime(nextConfig.type, providerId, trimmedKey);
} else {
await deleteApiKey(providerId);
await providerService.deleteLegacyProviderApiKey(providerId);
await syncDeletedProviderApiKeyToRuntime(existing, providerId);
}
}
@@ -275,14 +265,14 @@ export async function handleProviderRoutes(
if (url.pathname.startsWith('/api/providers/') && req.method === 'DELETE') {
const providerId = decodeURIComponent(url.pathname.slice('/api/providers/'.length));
try {
const existing = await getProvider(providerId);
const existing = await providerService.getLegacyProvider(providerId);
if (url.searchParams.get('apiKeyOnly') === '1') {
await deleteApiKey(providerId);
await providerService.deleteLegacyProviderApiKey(providerId);
await syncDeletedProviderApiKeyToRuntime(existing, providerId);
sendJson(res, 200, { success: true });
return true;
}
await deleteProvider(providerId);
await providerService.deleteLegacyProvider(providerId);
await syncDeletedProviderToRuntime(existing, providerId, ctx.gatewayManager);
sendJson(res, 200, { success: true });
} catch (error) {

View File

@@ -50,6 +50,7 @@ import {
} from '../services/providers/provider-runtime-sync';
import { validateApiKeyWithProvider } from '../services/providers/provider-validation';
import { appUpdater } from './updater';
import { PORTS } from '../utils/config';
type AppRequest = {
id?: string;
@@ -79,6 +80,7 @@ export function registerIpcHandlers(
): void {
// Unified request protocol (non-breaking: legacy channels remain available)
registerUnifiedRequestHandlers(gatewayManager);
registerHostApiProxyHandlers();
// Gateway handlers
registerGatewayHandlers(gatewayManager, mainWindow);
@@ -135,6 +137,68 @@ export function registerIpcHandlers(
registerFileHandlers();
}
type HostApiFetchRequest = {
path: string;
method?: string;
headers?: Record<string, string>;
body?: unknown;
};
function registerHostApiProxyHandlers(): void {
ipcMain.handle('hostapi:fetch', async (_, request: HostApiFetchRequest) => {
try {
const path = typeof request?.path === 'string' ? request.path : '';
if (!path || !path.startsWith('/')) {
throw new Error(`Invalid host API path: ${String(request?.path)}`);
}
const method = (request.method || 'GET').toUpperCase();
const headers: Record<string, string> = { ...(request.headers || {}) };
let body: BodyInit | undefined;
if (request.body !== undefined && request.body !== null) {
if (typeof request.body === 'string') {
body = request.body;
} else {
body = JSON.stringify(request.body);
if (!headers['Content-Type'] && !headers['content-type']) {
headers['Content-Type'] = 'application/json';
}
}
}
const response = await fetch(`http://127.0.0.1:${PORTS.CLAWX_HOST_API}${path}`, {
method,
headers,
body,
});
const data: { status: number; ok: boolean; json?: unknown; text?: string } = {
status: response.status,
ok: response.ok,
};
if (response.status !== 204) {
const contentType = response.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
data.json = await response.json().catch(() => undefined);
} else {
data.text = await response.text().catch(() => '');
}
}
return { ok: true, data };
} catch (error) {
return {
ok: false,
error: {
message: error instanceof Error ? error.message : String(error),
},
};
}
});
}
function mapAppErrorCode(error: unknown): AppResponse['error']['code'] {
const msg = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
if (msg.includes('timeout')) return 'TIMEOUT';
@@ -156,6 +220,7 @@ function isProxyKey(key: keyof AppSettings): boolean {
}
function registerUnifiedRequestHandlers(gatewayManager: GatewayManager): void {
const providerService = getProviderService();
const handleProxySettingsChange = async () => {
const settings = await getAllSettings();
await applyProxySettings(settings);
@@ -194,32 +259,32 @@ function registerUnifiedRequestHandlers(gatewayManager: GatewayManager): void {
}
case 'provider': {
if (request.action === 'list') {
data = await getAllProvidersWithKeyInfo();
data = await providerService.listLegacyProvidersWithKeyInfo();
break;
}
if (request.action === 'get') {
const payload = request.payload as { providerId?: string } | string | undefined;
const providerId = typeof payload === 'string' ? payload : payload?.providerId;
if (!providerId) throw new Error('Invalid provider.get payload');
data = await getProvider(providerId);
data = await providerService.getLegacyProvider(providerId);
break;
}
if (request.action === 'getDefault') {
data = await getDefaultProvider();
data = await providerService.getDefaultLegacyProvider();
break;
}
if (request.action === 'hasApiKey') {
const payload = request.payload as { providerId?: string } | string | undefined;
const providerId = typeof payload === 'string' ? payload : payload?.providerId;
if (!providerId) throw new Error('Invalid provider.hasApiKey payload');
data = await hasApiKey(providerId);
data = await providerService.hasLegacyProviderApiKey(providerId);
break;
}
if (request.action === 'getApiKey') {
const payload = request.payload as { providerId?: string } | string | undefined;
const providerId = typeof payload === 'string' ? payload : payload?.providerId;
if (!providerId) throw new Error('Invalid provider.getApiKey payload');
data = await getApiKey(providerId);
data = await providerService.getLegacyProviderApiKey(providerId);
break;
}
if (request.action === 'validateKey') {
@@ -234,7 +299,7 @@ function registerUnifiedRequestHandlers(gatewayManager: GatewayManager): void {
throw new Error('Invalid provider.validateKey payload');
}
const provider = await getProvider(providerId);
const provider = await providerService.getLegacyProvider(providerId);
const providerType = provider?.type || providerId;
const registryBaseUrl = getProviderConfig(providerType)?.baseUrl;
const resolvedBaseUrl = options?.baseUrl || provider?.baseUrl || registryBaseUrl;
@@ -251,12 +316,12 @@ function registerUnifiedRequestHandlers(gatewayManager: GatewayManager): void {
if (!config) throw new Error('Invalid provider.save payload');
try {
await saveProvider(config);
await providerService.saveLegacyProvider(config);
if (apiKey !== undefined) {
const trimmedKey = apiKey.trim();
if (trimmedKey) {
await storeApiKey(config.id, trimmedKey);
await providerService.setLegacyProviderApiKey(config.id, trimmedKey);
}
}
@@ -278,8 +343,8 @@ function registerUnifiedRequestHandlers(gatewayManager: GatewayManager): void {
if (!providerId) throw new Error('Invalid provider.delete payload');
try {
const existing = await getProvider(providerId);
await deleteProvider(providerId);
const existing = await providerService.getLegacyProvider(providerId);
await providerService.deleteLegacyProvider(providerId);
if (existing?.type) {
try {
await syncDeletedProviderToRuntime(existing, providerId, gatewayManager);
@@ -303,8 +368,8 @@ function registerUnifiedRequestHandlers(gatewayManager: GatewayManager): void {
if (!providerId || typeof apiKey !== 'string') throw new Error('Invalid provider.setApiKey payload');
try {
await storeApiKey(providerId, apiKey);
const provider = await getProvider(providerId);
await providerService.setLegacyProviderApiKey(providerId, apiKey);
const provider = await providerService.getLegacyProvider(providerId);
const providerType = provider?.type || providerId;
const ock = getOpenClawProviderKey(providerType, providerId);
try {
@@ -328,13 +393,13 @@ function registerUnifiedRequestHandlers(gatewayManager: GatewayManager): void {
const apiKey = Array.isArray(payload) ? payload[2] : payload?.apiKey;
if (!providerId || !updates) throw new Error('Invalid provider.updateWithKey payload');
const existing = await getProvider(providerId);
const existing = await providerService.getLegacyProvider(providerId);
if (!existing) {
data = { success: false, error: 'Provider not found' };
break;
}
const previousKey = await getApiKey(providerId);
const previousKey = await providerService.getLegacyProviderApiKey(providerId);
const previousOck = getOpenClawProviderKey(existing.type, providerId);
try {
@@ -344,15 +409,15 @@ function registerUnifiedRequestHandlers(gatewayManager: GatewayManager): void {
updatedAt: new Date().toISOString(),
};
const ock = getOpenClawProviderKey(nextConfig.type, providerId);
await saveProvider(nextConfig);
await providerService.saveLegacyProvider(nextConfig);
if (apiKey !== undefined) {
const trimmedKey = apiKey.trim();
if (trimmedKey) {
await storeApiKey(providerId, trimmedKey);
await providerService.setLegacyProviderApiKey(providerId, trimmedKey);
await saveProviderKeyToOpenClaw(ock, trimmedKey);
} else {
await deleteApiKey(providerId);
await providerService.deleteLegacyProviderApiKey(providerId);
await removeProviderFromOpenClaw(ock);
}
}
@@ -366,12 +431,12 @@ function registerUnifiedRequestHandlers(gatewayManager: GatewayManager): void {
data = { success: true };
} catch (error) {
try {
await saveProvider(existing);
await providerService.saveLegacyProvider(existing);
if (previousKey) {
await storeApiKey(providerId, previousKey);
await providerService.setLegacyProviderApiKey(providerId, previousKey);
await saveProviderKeyToOpenClaw(previousOck, previousKey);
} else {
await deleteApiKey(providerId);
await providerService.deleteLegacyProviderApiKey(providerId);
await removeProviderFromOpenClaw(previousOck);
}
} catch (rollbackError) {
@@ -387,8 +452,8 @@ function registerUnifiedRequestHandlers(gatewayManager: GatewayManager): void {
const providerId = typeof payload === 'string' ? payload : payload?.providerId;
if (!providerId) throw new Error('Invalid provider.deleteApiKey payload');
try {
await deleteApiKey(providerId);
const provider = await getProvider(providerId);
await providerService.deleteLegacyProviderApiKey(providerId);
const provider = await providerService.getLegacyProvider(providerId);
const providerType = provider?.type || providerId;
const ock = getOpenClawProviderKey(providerType, providerId);
try {
@@ -410,8 +475,8 @@ function registerUnifiedRequestHandlers(gatewayManager: GatewayManager): void {
if (!providerId) throw new Error('Invalid provider.setDefault payload');
try {
await setDefaultProvider(providerId);
const provider = await getProvider(providerId);
await providerService.setDefaultLegacyProvider(providerId);
const provider = await providerService.getLegacyProvider(providerId);
if (provider) {
try {
await syncDefaultProviderToRuntime(providerId, gatewayManager);
@@ -2422,4 +2487,4 @@ function registerSessionHandlers(): void {
return { success: false, error: String(err) };
}
});
}
}

View File

@@ -0,0 +1,48 @@
import { getProviderConfig } from '../utils/provider-registry';
import { getOpenClawProviderKeyForType, isOAuthProviderType } from '../utils/provider-keys';
import type { ProviderConfig } from '../utils/secure-storage';
export interface AgentProviderUpdatePayload {
providerKey: string;
entry: {
baseUrl: string;
api: string;
apiKey: string | undefined;
models: Array<{ id: string; name: string }>;
};
}
export function getModelIdFromRef(modelRef: string | undefined, providerKey: string): string | undefined {
if (!modelRef) return undefined;
if (modelRef.startsWith(`${providerKey}/`)) {
return modelRef.slice(providerKey.length + 1);
}
return modelRef;
}
export function buildNonOAuthAgentProviderUpdate(
provider: ProviderConfig,
providerId: string,
modelRef: string | undefined
): AgentProviderUpdatePayload | null {
if (provider.type === 'custom' || provider.type === 'ollama' || isOAuthProviderType(provider.type)) {
return null;
}
const providerKey = getOpenClawProviderKeyForType(provider.type, providerId);
const meta = getProviderConfig(provider.type);
const baseUrl = provider.baseUrl || meta?.baseUrl;
const api = meta?.api;
if (!baseUrl || !api) return null;
const modelId = getModelIdFromRef(modelRef, providerKey);
return {
providerKey,
entry: {
baseUrl,
api,
apiKey: meta?.apiKeyEnv,
models: modelId ? [{ id: modelId, name: modelId }] : [],
},
};
}

View File

@@ -22,6 +22,7 @@ const electronAPI = {
'gateway:restart',
'gateway:rpc',
'gateway:httpProxy',
'hostapi:fetch',
'gateway:health',
'gateway:getControlUiUrl',
// OpenClaw

View File

@@ -18,6 +18,12 @@ import { logger } from '../../utils/logger';
const GOOGLE_OAUTH_RUNTIME_PROVIDER = 'google-gemini-cli';
const GOOGLE_OAUTH_DEFAULT_MODEL_REF = `${GOOGLE_OAUTH_RUNTIME_PROVIDER}/gemini-3-pro-preview`;
type RuntimeProviderSyncContext = {
runtimeProviderKey: string;
meta: ReturnType<typeof getProviderConfig>;
api: string;
};
export function getOpenClawProviderKey(type: string, providerId: string): string {
if (type === 'custom' || type === 'ollama') {
const suffix = providerId.replace(/-/g, '').slice(0, 8);
@@ -172,63 +178,119 @@ export async function syncAllProviderAuthToRuntime(): Promise<void> {
}
}
export async function syncSavedProviderToRuntime(
async function syncProviderSecretToRuntime(
config: ProviderConfig,
runtimeProviderKey: string,
apiKey: string | undefined,
gatewayManager?: GatewayManager,
): Promise<void> {
const ock = await resolveRuntimeProviderKey(config);
const secret = await getProviderSecret(config.id);
if (apiKey !== undefined) {
const trimmedKey = apiKey.trim();
if (trimmedKey) {
await saveProviderKeyToOpenClaw(ock, trimmedKey);
await saveProviderKeyToOpenClaw(runtimeProviderKey, trimmedKey);
}
} else if (secret?.type === 'api_key') {
await saveProviderKeyToOpenClaw(ock, secret.apiKey);
} else if (secret?.type === 'oauth') {
await saveOAuthTokenToOpenClaw(ock, {
return;
}
if (secret?.type === 'api_key') {
await saveProviderKeyToOpenClaw(runtimeProviderKey, secret.apiKey);
return;
}
if (secret?.type === 'oauth') {
await saveOAuthTokenToOpenClaw(runtimeProviderKey, {
access: secret.accessToken,
refresh: secret.refreshToken,
expires: secret.expiresAt,
email: secret.email,
projectId: secret.subject,
});
} else if (secret?.type === 'local' && secret.apiKey) {
await saveProviderKeyToOpenClaw(ock, secret.apiKey);
}
const meta = getProviderConfig(config.type);
const api = config.type === 'custom' || config.type === 'ollama' ? 'openai-completions' : meta?.api;
if (!api) {
return;
}
await syncProviderConfigToOpenClaw(ock, config.model, {
baseUrl: config.baseUrl || meta?.baseUrl,
api,
apiKeyEnv: meta?.apiKeyEnv,
headers: meta?.headers,
});
if (secret?.type === 'local' && secret.apiKey) {
await saveProviderKeyToOpenClaw(runtimeProviderKey, secret.apiKey);
}
}
if (config.type === 'custom' || config.type === 'ollama') {
const resolvedKey = apiKey !== undefined ? (apiKey.trim() || null) : await getApiKey(config.id);
if (resolvedKey && config.baseUrl) {
const modelId = config.model;
await updateAgentModelProvider(ock, {
baseUrl: config.baseUrl,
api: 'openai-completions',
models: modelId ? [{ id: modelId, name: modelId }] : [],
apiKey: resolvedKey,
});
}
async function resolveRuntimeSyncContext(config: ProviderConfig): Promise<RuntimeProviderSyncContext | null> {
const runtimeProviderKey = await resolveRuntimeProviderKey(config);
const meta = getProviderConfig(config.type);
const api = config.type === 'custom' || config.type === 'ollama' ? 'openai-completions' : meta?.api;
if (!api) {
return null;
}
return {
runtimeProviderKey,
meta,
api,
};
}
async function syncRuntimeProviderConfig(
config: ProviderConfig,
context: RuntimeProviderSyncContext,
): Promise<void> {
await syncProviderConfigToOpenClaw(context.runtimeProviderKey, config.model, {
baseUrl: config.baseUrl || context.meta?.baseUrl,
api: context.api,
apiKeyEnv: context.meta?.apiKeyEnv,
headers: context.meta?.headers,
});
}
async function syncCustomProviderAgentModel(
config: ProviderConfig,
runtimeProviderKey: string,
apiKey: string | undefined,
): Promise<void> {
if (config.type !== 'custom' && config.type !== 'ollama') {
return;
}
const resolvedKey = apiKey !== undefined ? (apiKey.trim() || null) : await getApiKey(config.id);
if (!resolvedKey || !config.baseUrl) {
return;
}
const modelId = config.model;
await updateAgentModelProvider(runtimeProviderKey, {
baseUrl: config.baseUrl,
api: 'openai-completions',
models: modelId ? [{ id: modelId, name: modelId }] : [],
apiKey: resolvedKey,
});
}
async function syncProviderToRuntime(
config: ProviderConfig,
apiKey: string | undefined,
): Promise<RuntimeProviderSyncContext | null> {
const context = await resolveRuntimeSyncContext(config);
if (!context) {
return null;
}
await syncProviderSecretToRuntime(config, context.runtimeProviderKey, apiKey);
await syncRuntimeProviderConfig(config, context);
await syncCustomProviderAgentModel(config, context.runtimeProviderKey, apiKey);
return context;
}
export async function syncSavedProviderToRuntime(
config: ProviderConfig,
apiKey: string | undefined,
gatewayManager?: GatewayManager,
): Promise<void> {
const context = await syncProviderToRuntime(config, apiKey);
if (!context) {
return;
}
scheduleGatewayRestart(
gatewayManager,
`Scheduling Gateway restart after saving provider "${ock}" config`,
`Scheduling Gateway restart after saving provider "${context.runtimeProviderKey}" config`,
);
}
@@ -237,54 +299,13 @@ export async function syncUpdatedProviderToRuntime(
apiKey: string | undefined,
gatewayManager?: GatewayManager,
): Promise<void> {
const ock = await resolveRuntimeProviderKey(config);
const fallbackModels = await getProviderFallbackModelRefs(config);
const meta = getProviderConfig(config.type);
const api = config.type === 'custom' || config.type === 'ollama' ? 'openai-completions' : meta?.api;
const secret = await getProviderSecret(config.id);
if (!api) {
const context = await syncProviderToRuntime(config, apiKey);
if (!context) {
return;
}
if (apiKey !== undefined) {
const trimmedKey = apiKey.trim();
if (trimmedKey) {
await saveProviderKeyToOpenClaw(ock, trimmedKey);
}
} else if (secret?.type === 'api_key') {
await saveProviderKeyToOpenClaw(ock, secret.apiKey);
} else if (secret?.type === 'oauth') {
await saveOAuthTokenToOpenClaw(ock, {
access: secret.accessToken,
refresh: secret.refreshToken,
expires: secret.expiresAt,
email: secret.email,
projectId: secret.subject,
});
} else if (secret?.type === 'local' && secret.apiKey) {
await saveProviderKeyToOpenClaw(ock, secret.apiKey);
}
await syncProviderConfigToOpenClaw(ock, config.model, {
baseUrl: config.baseUrl || meta?.baseUrl,
api,
apiKeyEnv: meta?.apiKeyEnv,
headers: meta?.headers,
});
if (config.type === 'custom' || config.type === 'ollama') {
const resolvedKey = apiKey !== undefined ? (apiKey.trim() || null) : await getApiKey(config.id);
if (resolvedKey && config.baseUrl) {
const modelId = config.model;
await updateAgentModelProvider(ock, {
baseUrl: config.baseUrl,
api: 'openai-completions',
models: modelId ? [{ id: modelId, name: modelId }] : [],
apiKey: resolvedKey,
});
}
}
const ock = context.runtimeProviderKey;
const fallbackModels = await getProviderFallbackModelRefs(config);
const defaultProviderId = await getDefaultProvider();
if (defaultProviderId === config.id) {

View File

@@ -20,11 +20,7 @@ import {
import {
deleteApiKey,
deleteProvider,
getAllProviders,
getAllProvidersWithKeyInfo,
getApiKey,
getDefaultProvider,
getProvider,
hasApiKey,
saveProvider,
setDefaultProvider,
@@ -32,6 +28,14 @@ import {
} from '../../utils/secure-storage';
import type { ProviderWithKeyInfo } from '../../shared/providers/types';
function maskApiKey(apiKey: string | null): string | null {
if (!apiKey) return null;
if (apiKey.length > 12) {
return `${apiKey.substring(0, 4)}${'*'.repeat(apiKey.length - 8)}${apiKey.substring(apiKey.length - 4)}`;
}
return '*'.repeat(apiKey.length);
}
export class ProviderService {
async listVendors(): Promise<ProviderDefinition[]> {
return PROVIDER_DEFINITIONS;
@@ -49,7 +53,7 @@ export class ProviderService {
async getDefaultAccountId(): Promise<string | undefined> {
await ensureProviderStoreMigrated();
return (await getDefaultProvider()) ?? getDefaultProviderAccountId();
return getDefaultProviderAccountId();
}
async createAccount(account: ProviderAccount, apiKey?: string): Promise<ProviderAccount> {
@@ -107,31 +111,54 @@ export class ProviderService {
}
async listLegacyProviders(): Promise<ProviderConfig[]> {
return getAllProviders();
await ensureProviderStoreMigrated();
const accounts = await listProviderAccounts();
return accounts.map(providerAccountToConfig);
}
async listLegacyProvidersWithKeyInfo(): Promise<ProviderWithKeyInfo[]> {
return getAllProvidersWithKeyInfo();
const providers = await this.listLegacyProviders();
const results: ProviderWithKeyInfo[] = [];
for (const provider of providers) {
const apiKey = await getApiKey(provider.id);
results.push({
...provider,
hasKey: !!apiKey,
keyMasked: maskApiKey(apiKey),
});
}
return results;
}
async getLegacyProvider(providerId: string): Promise<ProviderConfig | null> {
return getProvider(providerId);
await ensureProviderStoreMigrated();
const account = await getProviderAccount(providerId);
return account ? providerAccountToConfig(account) : null;
}
async saveLegacyProvider(config: ProviderConfig): Promise<void> {
await saveProvider(config);
await ensureProviderStoreMigrated();
const account = providerConfigToAccount(config);
const existing = await getProviderAccount(config.id);
if (existing) {
await this.updateAccount(config.id, account);
return;
}
await this.createAccount(account);
}
async deleteLegacyProvider(providerId: string): Promise<boolean> {
return deleteProvider(providerId);
await ensureProviderStoreMigrated();
await this.deleteAccount(providerId);
return true;
}
async setDefaultLegacyProvider(providerId: string): Promise<void> {
await setDefaultProvider(providerId);
await this.setDefaultAccount(providerId);
}
async getDefaultLegacyProvider(): Promise<string | undefined> {
return getDefaultProvider();
return this.getDefaultAccountId();
}
async setLegacyProviderApiKey(providerId: string, apiKey: string): Promise<boolean> {

View File

@@ -386,21 +386,14 @@ export async function setOpenClawDefaultModel(
const config = await readOpenClawJson();
ensureMoonshotKimiWebSearchCnBaseUrl(config, provider);
const rawModel = modelOverride || getProviderDefaultModel(provider);
const model = rawModel
? (rawModel.startsWith(`${provider}/`) ? rawModel : `${provider}/${rawModel}`)
: undefined;
const model = normalizeModelRef(provider, modelOverride);
if (!model) {
console.warn(`No default model mapping for provider "${provider}"`);
return;
}
const modelId = model.startsWith(`${provider}/`)
? model.slice(provider.length + 1)
: model;
const fallbackModelIds = fallbackModels
.filter((fallback) => fallback.startsWith(`${provider}/`))
.map((fallback) => fallback.slice(provider.length + 1));
const modelId = extractModelId(provider, model);
const fallbackModelIds = extractFallbackModelIds(provider, fallbackModels);
// Set the default model for the agents
const agents = (config.agents || {}) as Record<string, unknown>;
@@ -415,51 +408,16 @@ export async function setOpenClawDefaultModel(
// Configure models.providers for providers that need explicit registration.
const providerCfg = getProviderConfig(provider);
if (providerCfg) {
const models = (config.models || {}) as Record<string, unknown>;
const providers = (models.providers || {}) as Record<string, unknown>;
const removedLegacyMoonshot = removeLegacyMoonshotProviderEntry(provider, providers);
const existingProvider =
providers[provider] && typeof providers[provider] === 'object'
? (providers[provider] as Record<string, unknown>)
: {};
const existingModels = Array.isArray(existingProvider.models)
? (existingProvider.models as Array<Record<string, unknown>>)
: [];
const registryModels = (providerCfg.models ?? []).map((m) => ({ ...m })) as Array<Record<string, unknown>>;
const mergedModels = [...registryModels];
for (const item of existingModels) {
const id = typeof item?.id === 'string' ? item.id : '';
if (id && !mergedModels.some((m) => m.id === id)) {
mergedModels.push(item);
}
}
for (const candidateModelId of [modelId, ...fallbackModelIds]) {
if (candidateModelId && !mergedModels.some((m) => m.id === candidateModelId)) {
mergedModels.push({ id: candidateModelId, name: candidateModelId });
}
}
const providerEntry: Record<string, unknown> = {
...existingProvider,
upsertOpenClawProviderEntry(config, provider, {
baseUrl: providerCfg.baseUrl,
api: providerCfg.api,
apiKey: providerCfg.apiKeyEnv,
models: mergedModels,
};
if (providerCfg.headers && Object.keys(providerCfg.headers).length > 0) {
providerEntry.headers = providerCfg.headers;
}
providers[provider] = providerEntry;
apiKeyEnv: providerCfg.apiKeyEnv,
headers: providerCfg.headers,
modelIds: [modelId, ...fallbackModelIds],
includeRegistryModels: true,
mergeExistingModels: true,
});
console.log(`Configured models.providers.${provider} with baseUrl=${providerCfg.baseUrl}, model=${modelId}`);
if (removedLegacyMoonshot) {
console.log('Removed legacy models.providers.moonshot alias entry');
}
models.providers = providers;
config.models = models;
} else {
// Built-in provider: remove any stale models.providers entry
const models = (config.models || {}) as Record<string, unknown>;
@@ -489,6 +447,99 @@ interface RuntimeProviderConfigOverride {
authHeader?: boolean;
}
type ProviderEntryBuildOptions = {
baseUrl: string;
api: string;
apiKeyEnv?: string;
headers?: Record<string, string>;
authHeader?: boolean;
modelIds?: string[];
includeRegistryModels?: boolean;
mergeExistingModels?: boolean;
};
function normalizeModelRef(provider: string, modelOverride?: string): string | undefined {
const rawModel = modelOverride || getProviderDefaultModel(provider);
if (!rawModel) return undefined;
return rawModel.startsWith(`${provider}/`) ? rawModel : `${provider}/${rawModel}`;
}
function extractModelId(provider: string, modelRef: string): string {
return modelRef.startsWith(`${provider}/`) ? modelRef.slice(provider.length + 1) : modelRef;
}
function extractFallbackModelIds(provider: string, fallbackModels: string[]): string[] {
return fallbackModels
.filter((fallback) => fallback.startsWith(`${provider}/`))
.map((fallback) => fallback.slice(provider.length + 1));
}
function mergeProviderModels(
...groups: Array<Array<Record<string, unknown>>>
): Array<Record<string, unknown>> {
const merged: Array<Record<string, unknown>> = [];
const seen = new Set<string>();
for (const group of groups) {
for (const item of group) {
const id = typeof item?.id === 'string' ? item.id : '';
if (!id || seen.has(id)) continue;
seen.add(id);
merged.push(item);
}
}
return merged;
}
function upsertOpenClawProviderEntry(
config: Record<string, unknown>,
provider: string,
options: ProviderEntryBuildOptions,
): void {
const models = (config.models || {}) as Record<string, unknown>;
const providers = (models.providers || {}) as Record<string, unknown>;
const removedLegacyMoonshot = removeLegacyMoonshotProviderEntry(provider, providers);
const existingProvider = (
providers[provider] && typeof providers[provider] === 'object'
? (providers[provider] as Record<string, unknown>)
: {}
);
const existingModels = options.mergeExistingModels && Array.isArray(existingProvider.models)
? (existingProvider.models as Array<Record<string, unknown>>)
: [];
const registryModels = options.includeRegistryModels
? ((getProviderConfig(provider)?.models ?? []).map((m) => ({ ...m })) as Array<Record<string, unknown>>)
: [];
const runtimeModels = (options.modelIds ?? []).map((id) => ({ id, name: id }));
const nextProvider: Record<string, unknown> = {
...existingProvider,
baseUrl: options.baseUrl,
api: options.api,
models: mergeProviderModels(registryModels, existingModels, runtimeModels),
};
if (options.apiKeyEnv) nextProvider.apiKey = options.apiKeyEnv;
if (options.headers && Object.keys(options.headers).length > 0) {
nextProvider.headers = options.headers;
} else {
delete nextProvider.headers;
}
if (options.authHeader !== undefined) {
nextProvider.authHeader = options.authHeader;
} else {
delete nextProvider.authHeader;
}
providers[provider] = nextProvider;
models.providers = providers;
config.models = models;
if (removedLegacyMoonshot) {
console.log('Removed legacy models.providers.moonshot alias entry');
}
}
function removeLegacyMoonshotProviderEntry(
_provider: string,
_providers: Record<string, unknown>
@@ -528,26 +579,13 @@ export async function syncProviderConfigToOpenClaw(
ensureMoonshotKimiWebSearchCnBaseUrl(config, provider);
if (override.baseUrl && override.api) {
const models = (config.models || {}) as Record<string, unknown>;
const providers = (models.providers || {}) as Record<string, unknown>;
removeLegacyMoonshotProviderEntry(provider, providers);
const nextModels: Array<Record<string, unknown>> = [];
if (modelId) nextModels.push({ id: modelId, name: modelId });
const nextProvider: Record<string, unknown> = {
upsertOpenClawProviderEntry(config, provider, {
baseUrl: override.baseUrl,
api: override.api,
models: nextModels,
};
if (override.apiKeyEnv) nextProvider.apiKey = override.apiKeyEnv;
if (override.headers && Object.keys(override.headers).length > 0) {
nextProvider.headers = override.headers;
}
providers[provider] = nextProvider;
models.providers = providers;
config.models = models;
apiKeyEnv: override.apiKeyEnv,
headers: override.headers,
modelIds: modelId ? [modelId] : [],
});
}
// Ensure extension is enabled for oauth providers to prevent gateway wiping config
@@ -580,21 +618,14 @@ export async function setOpenClawDefaultModelWithOverride(
const config = await readOpenClawJson();
ensureMoonshotKimiWebSearchCnBaseUrl(config, provider);
const rawModel = modelOverride || getProviderDefaultModel(provider);
const model = rawModel
? (rawModel.startsWith(`${provider}/`) ? rawModel : `${provider}/${rawModel}`)
: undefined;
const model = normalizeModelRef(provider, modelOverride);
if (!model) {
console.warn(`No default model mapping for provider "${provider}"`);
return;
}
const modelId = model.startsWith(`${provider}/`)
? model.slice(provider.length + 1)
: model;
const fallbackModelIds = fallbackModels
.filter((fallback) => fallback.startsWith(`${provider}/`))
.map((fallback) => fallback.slice(provider.length + 1));
const modelId = extractModelId(provider, model);
const fallbackModelIds = extractFallbackModelIds(provider, fallbackModels);
const agents = (config.agents || {}) as Record<string, unknown>;
const defaults = (agents.defaults || {}) as Record<string, unknown>;
@@ -606,33 +637,14 @@ export async function setOpenClawDefaultModelWithOverride(
config.agents = agents;
if (override.baseUrl && override.api) {
const models = (config.models || {}) as Record<string, unknown>;
const providers = (models.providers || {}) as Record<string, unknown>;
removeLegacyMoonshotProviderEntry(provider, providers);
const nextModels: Array<Record<string, unknown>> = [];
for (const candidateModelId of [modelId, ...fallbackModelIds]) {
if (candidateModelId && !nextModels.some((entry) => entry.id === candidateModelId)) {
nextModels.push({ id: candidateModelId, name: candidateModelId });
}
}
const nextProvider: Record<string, unknown> = {
upsertOpenClawProviderEntry(config, provider, {
baseUrl: override.baseUrl,
api: override.api,
models: nextModels,
};
if (override.apiKeyEnv) nextProvider.apiKey = override.apiKeyEnv;
if (override.headers && Object.keys(override.headers).length > 0) {
nextProvider.headers = override.headers;
}
if (override.authHeader !== undefined) {
nextProvider.authHeader = override.authHeader;
}
providers[provider] = nextProvider;
models.providers = providers;
config.models = models;
apiKeyEnv: override.apiKeyEnv,
headers: override.headers,
authHeader: override.authHeader,
modelIds: [modelId, ...fallbackModelIds],
});
}
const gateway = (config.gateway || {}) as Record<string, unknown>;

View File

@@ -36,7 +36,6 @@ export interface AppSettings {
proxyHttpsServer: string;
proxyAllServer: string;
proxyBypassRules: string;
gatewayTransportPreference: 'ws-first' | 'http-first' | 'ws-only' | 'http-only' | 'ipc-only';
// Update
updateChannel: 'stable' | 'beta' | 'dev';
@@ -74,7 +73,6 @@ const defaults: AppSettings = {
proxyHttpsServer: '',
proxyAllServer: '',
proxyBypassRules: '<local>;localhost;127.0.0.1;::1',
gatewayTransportPreference: 'ws-first',
// Update
updateChannel: 'stable',